├── compiler ├── .gitignore ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ ├── gradle │ │ │ └── incremental.annotation.processors │ │ │ └── services │ │ │ └── javax.annotation.processing.Processor │ │ └── java │ │ └── com │ │ └── kpstv │ │ └── processor │ │ ├── utils │ │ ├── AutoGeneratorType.kt │ │ ├── AutoGeneratorDataType.kt │ │ ├── Extensions.kt │ │ ├── Utils.kt │ │ ├── ClickUtils.kt │ │ └── Consts.kt │ │ ├── generators │ │ ├── ItemCountGenerator.kt │ │ ├── ViewTypeGenerator.kt │ │ ├── ViewHolderGenerator.kt │ │ ├── auto │ │ │ ├── EnumGenericConverter.kt │ │ │ ├── SQLDelightAdapterProcessor.kt │ │ │ └── TypeConverterProcessor.kt │ │ ├── DiffUtilGenerator.kt │ │ ├── TypeConverterGenerator.kt │ │ └── BindViewGenerator.kt │ │ ├── abstract │ │ └── BaseAutoGenerator.kt │ │ └── BindingProcessor.kt └── build.gradle ├── lint ├── src │ ├── .gitignore │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── services │ │ │ │ └── com │ │ │ │ └── android │ │ │ │ └── tools │ │ │ │ └── lint │ │ │ │ └── client │ │ │ │ └── api │ │ │ │ └── IssueRegistry │ │ └── java │ │ │ └── com │ │ │ └── kpstv │ │ │ └── lint │ │ │ ├── Utils.kt │ │ │ ├── IssueRegistry.kt │ │ │ └── detectors │ │ │ ├── TypeConvertDetector.kt │ │ │ └── RecyclerViewDetector.kt │ └── test │ │ └── java │ │ └── com │ │ └── kpstv │ │ └── lint │ │ ├── CorrectRecyclerViewListImplementationTests.kt │ │ ├── CorrectAutogenerateConverterTests.kt │ │ ├── CorrectRecyclerViewImplementationTests.kt │ │ ├── WrongAutogenerateConverterTests.kt │ │ ├── WrongAutoGenerateSQLAdapterTests.kt │ │ ├── WrongRecyclerViewListImplementationTests.kt │ │ ├── WrongRecyclerViewImplementationTests.kt │ │ └── utils │ │ └── Stubs.kt ├── .gitignore └── build.gradle ├── sample-ktx ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ ├── item_big_layout.xml │ │ │ └── item_small_layout.xml │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ └── drawable │ │ │ └── ic_launcher_background.xml │ │ ├── java │ │ └── com │ │ │ └── kpstv │ │ │ └── sample_ktx │ │ │ ├── SQLDelightAdapters.kt │ │ │ ├── data │ │ │ ├── DemoDatabase.kt │ │ │ └── Data.kt │ │ │ ├── Utils.kt │ │ │ └── MainActivity.kt │ │ ├── sqldelight │ │ └── com │ │ │ └── kpstv │ │ │ └── sample_ktx │ │ │ └── data │ │ │ └── DataDomain.sq │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle ├── autobindings-room ├── .gitignore ├── src │ └── main │ │ └── AndroidManifest.xml └── build.gradle ├── autobindings-recyclerview ├── .gitignore ├── src │ └── main │ │ └── AndroidManifest.xml └── build.gradle ├── autobindings-room-noop ├── .gitignore ├── consumer-rules.pro ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── androidx │ │ └── room │ │ └── TypeConverter.kt ├── proguard-rules.pro └── build.gradle ├── autobindings-sqldelight ├── .gitignore ├── src │ └── main │ │ └── AndroidManifest.xml └── build.gradle ├── autobindings-annotations-room ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── com │ └── kpstv │ └── bindings │ └── AutoGenerateConverter.kt ├── autobindings-annotations-sqldelight ├── .gitignore ├── src │ └── main │ │ └── java │ │ └── com │ │ └── kpstv │ │ └── bindings │ │ └── AutoGenerateSQLDelight.kt └── build.gradle ├── autobindings-annotations-converter-core ├── .gitignore ├── src │ └── main │ │ └── java │ │ └── com │ │ └── kpstv │ │ └── bindings │ │ └── ConverterType.kt └── build.gradle ├── autobindings-annotations-recyclerview ├── .gitignore ├── src │ └── main │ │ └── java │ │ └── com │ │ └── kpstv │ │ └── bindings │ │ ├── RecyclerViewAdapter.kt │ │ ├── RecyclerViewListAdapter.kt │ │ ├── Annotations.kt │ │ ├── DiffUtilAnnotation.kt │ │ ├── ClickAnnotation.kt │ │ └── GlideLoad.kt └── build.gradle ├── .gitmodules ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── .github └── workflows │ ├── build.yml │ └── main.yml ├── settings.gradle ├── gradle.properties ├── gradlew.bat ├── README.md ├── publish.gradle ├── gradlew └── LICENSE /compiler/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lint/src/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample-ktx/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /autobindings-room/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /autobindings-recyclerview/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /autobindings-room-noop/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /autobindings-room-noop/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /autobindings-sqldelight/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /autobindings-annotations-room/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /autobindings-annotations-sqldelight/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /autobindings-annotations-converter-core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /autobindings-annotations-recyclerview/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs"] 2 | path = docs 3 | url = https://github.com/KaustubhPatange/AutoBindings.wiki.git 4 | -------------------------------------------------------------------------------- /compiler/src/main/resources/META-INF/gradle/incremental.annotation.processors: -------------------------------------------------------------------------------- 1 | com.kpstv.processor.BindingProcessor,dynamic -------------------------------------------------------------------------------- /compiler/src/main/resources/META-INF/services/javax.annotation.processing.Processor: -------------------------------------------------------------------------------- 1 | com.kpstv.processor.BindingProcessor -------------------------------------------------------------------------------- /sample-ktx/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | sample-ktx 3 | -------------------------------------------------------------------------------- /lint/src/main/resources/META-INF/services/com/android/tools/lint/client/api/IssueRegistry: -------------------------------------------------------------------------------- 1 | com.kpstv.lint.IssueRegistry 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaustubhPatange/AutoBindings/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /autobindings-room/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /autobindings-recyclerview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /autobindings-sqldelight/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample-ktx/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaustubhPatange/AutoBindings/HEAD/sample-ktx/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-ktx/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaustubhPatange/AutoBindings/HEAD/sample-ktx/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-ktx/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaustubhPatange/AutoBindings/HEAD/sample-ktx/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-ktx/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaustubhPatange/AutoBindings/HEAD/sample-ktx/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-ktx/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaustubhPatange/AutoBindings/HEAD/sample-ktx/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-ktx/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaustubhPatange/AutoBindings/HEAD/sample-ktx/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample-ktx/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaustubhPatange/AutoBindings/HEAD/sample-ktx/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/utils/AutoGeneratorType.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.utils 2 | 3 | enum class AutoGeneratorType { 4 | ROOM, 5 | SQLDelight 6 | } -------------------------------------------------------------------------------- /sample-ktx/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaustubhPatange/AutoBindings/HEAD/sample-ktx/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample-ktx/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaustubhPatange/AutoBindings/HEAD/sample-ktx/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample-ktx/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaustubhPatange/AutoBindings/HEAD/sample-ktx/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /autobindings-annotations-converter-core/src/main/java/com/kpstv/bindings/ConverterType.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.bindings 2 | 3 | enum class ConverterType { 4 | GSON, MOSHI, KOTLIN_SERIALIZATION 5 | } -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/utils/AutoGeneratorDataType.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.utils 2 | 3 | enum class AutoGeneratorDataType { 4 | DATA, 5 | LIST, 6 | MAP, 7 | PAIR 8 | } -------------------------------------------------------------------------------- /autobindings-room-noop/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Mar 31 12:40:23 IST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 7 | -------------------------------------------------------------------------------- /autobindings-room-noop/src/main/java/androidx/room/TypeConverter.kt: -------------------------------------------------------------------------------- 1 | package androidx.room 2 | 3 | /** 4 | * Blob @TypeConverter annotation. 5 | */ 6 | @Target(AnnotationTarget.FUNCTION, 7 | AnnotationTarget.PROPERTY_GETTER, 8 | AnnotationTarget.PROPERTY_SETTER) 9 | annotation class TypeConverter -------------------------------------------------------------------------------- /sample-ktx/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | 7 | #4A4646 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | /key.asc 17 | *.gpg 18 | lint-report.html 19 | -------------------------------------------------------------------------------- /sample-ktx/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample-ktx/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lint/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | /key.asc 16 | *.gpg 17 | publish-aar.gradle 18 | publish-jar.gradle -------------------------------------------------------------------------------- /sample-ktx/src/main/java/com/kpstv/sample_ktx/SQLDelightAdapters.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.sample_ktx 2 | 3 | import com.kpstv.bindings.* 4 | import com.squareup.sqldelight.ColumnAdapter 5 | 6 | @AutoGenerateSQLDelightAdapters(using = ConverterType.KOTLIN_SERIALIZATION) 7 | interface SQLDelightAdapters { 8 | fun tagsConverter(): ColumnAdapter, String> 9 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push] 3 | jobs: 4 | buildJob: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Set up JDK 11 9 | uses: actions/setup-java@v1 10 | with: 11 | java-version: 11 12 | - run: chmod +x gradlew 13 | - run: chmod -R 777 ./ 14 | # Build project & run tests 15 | - run: ./gradlew build 16 | -------------------------------------------------------------------------------- /sample-ktx/src/main/sqldelight/com/kpstv/sample_ktx/data/DataDomain.sq: -------------------------------------------------------------------------------- 1 | import kotlin.collections.List; 2 | 3 | CREATE TABLE DataDomain ( 4 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 5 | name TEXT NOT NULL, 6 | imageUrl TEXT NOT NULL, 7 | tags TEXT AS List NOT NULL 8 | ); 9 | 10 | deleteAll: 11 | DELETE FROM DataDomain; 12 | 13 | insert: 14 | INSERT INTO DataDomain(name, imageUrl, tags) 15 | VALUES ?; -------------------------------------------------------------------------------- /sample-ktx/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':lint' 2 | include ':sample-ktx' 3 | include ':autobindings-annotations-recyclerview' 4 | include ':autobindings-annotations-room' 5 | include ':autobindings-annotations-sqldelight' 6 | include ':autobindings-annotations-converter-core' 7 | include ':autobindings-sqldelight' 8 | include ':autobindings-room' 9 | include ':autobindings-recyclerview' 10 | include ':compiler' 11 | include ':autobindings-room-noop' 12 | rootProject.name = "AutoBindings" 13 | -------------------------------------------------------------------------------- /autobindings-annotations-sqldelight/src/main/java/com/kpstv/bindings/AutoGenerateSQLDelight.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.bindings 2 | 3 | /** 4 | * Automatically generate [SQLDelightAdapter] for SQLDelight. 5 | * 6 | * @see Documentation 7 | */ 8 | @Target(AnnotationTarget.CLASS) 9 | @Retention(AnnotationRetention.SOURCE) 10 | annotation class AutoGenerateSQLDelightAdapters(val using: ConverterType) -------------------------------------------------------------------------------- /sample-ktx/src/main/java/com/kpstv/sample_ktx/data/DemoDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.sample_ktx.data 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | 7 | @Database( 8 | entities = [ 9 | TestDataClass::class 10 | ], 11 | version = 1, 12 | exportSchema = false 13 | ) 14 | @TypeConverters( 15 | com.kpstv.sample_ktx.data.TestDataClassConverter::class, 16 | ) 17 | abstract class DemoDatabase : RoomDatabase() -------------------------------------------------------------------------------- /autobindings-annotations-recyclerview/src/main/java/com/kpstv/bindings/RecyclerViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.bindings 2 | 3 | import kotlin.reflect.KClass 4 | 5 | /** 6 | * Automatically generates standard RecyclerView Adapter. 7 | * 8 | * Code example (Kotlin) 9 | * ``` 10 | * @RecyclerViewAdapter(POJO::class.java) 11 | * class TestAdapter { 12 | * ... 13 | * } 14 | * ``` 15 | * 16 | * This will generate `BindTestAdapter` class which can be used with 17 | * RecyclerView. 18 | */ 19 | @Target(AnnotationTarget.CLASS) 20 | @Retention(AnnotationRetention.SOURCE) 21 | annotation class RecyclerViewAdapter(val dataSetType: KClass<*>) -------------------------------------------------------------------------------- /autobindings-annotations-recyclerview/src/main/java/com/kpstv/bindings/RecyclerViewListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.bindings 2 | 3 | import kotlin.reflect.KClass 4 | 5 | /** 6 | * Automatically generates modern RecyclerView Adapter. 7 | * 8 | * Code example (Kotlin) 9 | * ``` 10 | * @RecyclerViewListAdapter(POJO::class.java) 11 | * class TestAdapter { 12 | * ... 13 | * } 14 | * ``` 15 | * 16 | * This will generate `BindTestAdapter` class which can be used with 17 | * RecyclerView. 18 | */ 19 | @Target(AnnotationTarget.CLASS) 20 | @Retention(AnnotationRetention.SOURCE) 21 | annotation class RecyclerViewListAdapter(val dataSetType: KClass<*>) -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/generators/ItemCountGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.generators 2 | 3 | import com.kpstv.processor.utils.Consts 4 | import com.squareup.javapoet.MethodSpec 5 | import com.squareup.javapoet.TypeName 6 | import javax.lang.model.element.Modifier 7 | 8 | object ItemCountGenerator { 9 | fun generateGetItemCountMethod(): MethodSpec { 10 | return MethodSpec.methodBuilder("getItemCount") 11 | .addModifiers(Modifier.PUBLIC) 12 | .addAnnotation(Override::class.java) 13 | .returns(TypeName.INT) 14 | .addStatement("return ${Consts.dataSet}.size()") 15 | .build() 16 | } 17 | } -------------------------------------------------------------------------------- /autobindings-annotations-recyclerview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'kotlin' 3 | 4 | dependencies { 5 | implementation fileTree(dir: 'libs', include: ['*.jar']) 6 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 7 | // implementation 'com.android.support:support-annotations:28.0.0' 8 | implementation 'androidx.annotation:annotation:1.1.0' 9 | } 10 | 11 | sourceCompatibility = "1.7" 12 | targetCompatibility = "1.7" 13 | 14 | ext { 15 | PUBLISH_GROUP_ID = 'io.github.kaustubhpatange' 16 | PUBLISH_ARTIFACT_ID = 'autobindings-annotations-recyclerview' 17 | PUBLISH_VERSION = versions.library 18 | } 19 | 20 | apply from: rootProject.file('publish.gradle') -------------------------------------------------------------------------------- /autobindings-annotations-converter-core/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'kotlin' 3 | 4 | dependencies { 5 | implementation fileTree(dir: 'libs', include: ['*.jar']) 6 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 7 | // implementation 'com.android.support:support-annotations:28.0.0' 8 | implementation 'androidx.annotation:annotation:1.1.0' 9 | } 10 | 11 | sourceCompatibility = "1.7" 12 | targetCompatibility = "1.7" 13 | 14 | ext { 15 | PUBLISH_GROUP_ID = 'io.github.kaustubhpatange' 16 | PUBLISH_ARTIFACT_ID = 'autobindings-annotations-converter-core' 17 | PUBLISH_VERSION = versions.library 18 | } 19 | 20 | apply from: rootProject.file('publish.gradle') -------------------------------------------------------------------------------- /autobindings-annotations-room/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'kotlin' 3 | 4 | dependencies { 5 | implementation fileTree(dir: 'libs', include: ['*.jar']) 6 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 7 | // implementation 'com.android.support:support-annotations:28.0.0' 8 | implementation 'androidx.annotation:annotation:1.1.0' 9 | api project(":autobindings-annotations-converter-core") 10 | } 11 | 12 | sourceCompatibility = "1.7" 13 | targetCompatibility = "1.7" 14 | 15 | ext { 16 | PUBLISH_GROUP_ID = 'io.github.kaustubhpatange' 17 | PUBLISH_ARTIFACT_ID = 'autobindings-annotations-room' 18 | PUBLISH_VERSION = versions.library 19 | } 20 | 21 | apply from: rootProject.file('publish.gradle') -------------------------------------------------------------------------------- /autobindings-annotations-recyclerview/src/main/java/com/kpstv/bindings/Annotations.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.bindings 2 | 3 | import androidx.annotation.LayoutRes 4 | 5 | /** 6 | * Code example (Kotlin) 7 | * ``` 8 | * @Bind(...) 9 | * fun bind(view: View, item: Data, position: Int): Unit 10 | * ``` 11 | */ 12 | @Target(AnnotationTarget.FUNCTION) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class OnBind(@LayoutRes val layoutId: Int, val viewType: Int = 0) 15 | 16 | /** 17 | * Works as getViewItemType in Adapter. 18 | * 19 | * Code example (Kotlin) 20 | * ``` 21 | * @ItemViewType 22 | * fun viewType(position: Int): Int 23 | * ``` 24 | */ 25 | @Target(AnnotationTarget.FUNCTION) 26 | @Retention(AnnotationRetention.SOURCE) 27 | annotation class ItemViewType 28 | -------------------------------------------------------------------------------- /autobindings-annotations-sqldelight/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'kotlin' 3 | 4 | dependencies { 5 | implementation fileTree(dir: 'libs', include: ['*.jar']) 6 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 7 | // implementation 'com.android.support:support-annotations:28.0.0' 8 | implementation 'androidx.annotation:annotation:1.1.0' 9 | api project(":autobindings-annotations-converter-core") 10 | } 11 | 12 | sourceCompatibility = "1.7" 13 | targetCompatibility = "1.7" 14 | 15 | ext { 16 | PUBLISH_GROUP_ID = 'io.github.kaustubhpatange' 17 | PUBLISH_ARTIFACT_ID = 'autobindings-annotations-sqldelight' 18 | PUBLISH_VERSION = versions.library 19 | } 20 | 21 | apply from: rootProject.file('publish.gradle') -------------------------------------------------------------------------------- /autobindings-annotations-recyclerview/src/main/java/com/kpstv/bindings/DiffUtilAnnotation.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.bindings 2 | 3 | /** 4 | * One of the method of [DiffUtil.ItemCallback] 5 | * 6 | * Code example (Kotlin) 7 | * ``` 8 | * @DiffItemSame 9 | * fun diffItemSame(oldItem: Data, newItemSame: Data): Boolean 10 | * ``` 11 | */ 12 | @Target(AnnotationTarget.FUNCTION) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class DiffItemSame 15 | 16 | /** 17 | * One of the method of [DiffUtil.ItemCallback] 18 | * 19 | * Code example (Kotlin) 20 | * ``` 21 | * @DiffContentSame 22 | * fun diffContentSame(oldItem: Data, newItemSame: Data): Boolean 23 | * ``` 24 | */ 25 | @Target(AnnotationTarget.FUNCTION) 26 | @Retention(AnnotationRetention.SOURCE) 27 | annotation class DiffContentSame -------------------------------------------------------------------------------- /sample-ktx/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /autobindings-room-noop/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /sample-ktx/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /compiler/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'kotlin' 3 | 4 | dependencies { 5 | implementation fileTree(dir: 'libs', include: ['*.jar']) 6 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 7 | implementation 'com.squareup:javapoet:1.12.1' 8 | 9 | implementation project(":autobindings-annotations-recyclerview") 10 | implementation project(":autobindings-annotations-room") 11 | implementation project(":autobindings-annotations-sqldelight") 12 | 13 | compileOnly 'com.google.android.material:material:1.2.0-beta01' 14 | } 15 | 16 | sourceCompatibility = "1.7" 17 | targetCompatibility = "1.7" 18 | 19 | ext { 20 | PUBLISH_GROUP_ID = 'io.github.kaustubhpatange' 21 | PUBLISH_ARTIFACT_ID = 'autobindings-compiler' 22 | PUBLISH_VERSION = versions.library 23 | } 24 | 25 | apply from: rootProject.file('publish.gradle') -------------------------------------------------------------------------------- /sample-ktx/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /sample-ktx/src/main/res/layout/item_big_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /autobindings-room-noop/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | id 'maven-publish' 5 | } 6 | 7 | android { 8 | compileSdkVersion 30 9 | 10 | buildFeatures.buildConfig false 11 | defaultConfig { 12 | minSdkVersion 16 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation 'androidx.core:core-ktx:1.6.0' 28 | } 29 | 30 | ext { 31 | PUBLISH_GROUP_ID = 'io.github.kaustubhpatange' 32 | PUBLISH_ARTIFACT_ID = 'autobindings-room-noop' 33 | PUBLISH_VERSION = versions.library 34 | } 35 | 36 | apply from: "${rootProject.projectDir}/publish.gradle" -------------------------------------------------------------------------------- /lint/src/main/java/com/kpstv/lint/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.lint 2 | 3 | import com.intellij.psi.PsiClass 4 | import com.kpstv.lint.detectors.RecyclerViewDetector 5 | import org.jetbrains.uast.UClass 6 | import org.jetbrains.uast.UMethod 7 | 8 | enum class Language { 9 | JAVA, 10 | KOTLIN 11 | } 12 | 13 | fun UClass.language(): Language? { 14 | return when (language.displayName) { 15 | "JAVA" -> Language.JAVA 16 | "kotlin" -> Language.KOTLIN 17 | else -> null 18 | } 19 | } 20 | 21 | object Utils { 22 | fun getSimpleName(qualifiedName: String?): String? { 23 | return qualifiedName?.substring(qualifiedName.lastIndexOf(".") + 1, qualifiedName.length) 24 | } 25 | 26 | fun findAnnotationValue(clazz: PsiClass?, name: String) = 27 | if (clazz != null && clazz.hasAnnotation(RecyclerViewDetector.ANNOTATION_RECYCLERVIEWLIST)) { 28 | clazz.getAnnotation(RecyclerViewDetector.ANNOTATION_RECYCLERVIEWLIST) 29 | ?.findAttributeValue(name)?.text ?: "Any" 30 | } else "Any" 31 | } -------------------------------------------------------------------------------- /autobindings-room/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | apply plugin: 'maven-publish' 5 | 6 | android { 7 | compileSdkVersion 30 8 | 9 | buildFeatures.buildConfig false 10 | defaultConfig { 11 | minSdkVersion 16 12 | } 13 | 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | } 18 | } 19 | compileOptions { 20 | sourceCompatibility JavaVersion.VERSION_1_8 21 | targetCompatibility JavaVersion.VERSION_1_8 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: "libs", include: ["*.jar"]) 27 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 28 | 29 | api project(":autobindings-annotations-room") 30 | lintPublish project(":lint") 31 | } 32 | 33 | ext { 34 | PUBLISH_GROUP_ID = 'io.github.kaustubhpatange' 35 | PUBLISH_ARTIFACT_ID = 'autobindings-room' 36 | PUBLISH_VERSION = versions.library 37 | } 38 | 39 | apply from: "${rootProject.projectDir}/publish.gradle" -------------------------------------------------------------------------------- /autobindings-sqldelight/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | apply plugin: 'maven-publish' 5 | 6 | android { 7 | compileSdkVersion 30 8 | 9 | buildFeatures.buildConfig false 10 | defaultConfig { 11 | minSdkVersion 16 12 | } 13 | 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | } 18 | } 19 | compileOptions { 20 | sourceCompatibility JavaVersion.VERSION_1_8 21 | targetCompatibility JavaVersion.VERSION_1_8 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: "libs", include: ["*.jar"]) 27 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 28 | 29 | api project(":autobindings-annotations-sqldelight") 30 | lintPublish project(":lint") 31 | } 32 | 33 | ext { 34 | PUBLISH_GROUP_ID = 'io.github.kaustubhpatange' 35 | PUBLISH_ARTIFACT_ID = 'autobindings-sqldelight' 36 | PUBLISH_VERSION = versions.library 37 | } 38 | 39 | apply from: "${rootProject.projectDir}/publish.gradle" -------------------------------------------------------------------------------- /autobindings-recyclerview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | apply plugin: 'maven-publish' 5 | 6 | android { 7 | compileSdkVersion 30 8 | 9 | buildFeatures.buildConfig false 10 | defaultConfig { 11 | minSdkVersion 16 12 | } 13 | 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | } 18 | } 19 | compileOptions { 20 | sourceCompatibility JavaVersion.VERSION_1_8 21 | targetCompatibility JavaVersion.VERSION_1_8 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: "libs", include: ["*.jar"]) 27 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 28 | 29 | api project(":autobindings-annotations-recyclerview") 30 | lintPublish project(":lint") 31 | } 32 | 33 | ext { 34 | PUBLISH_GROUP_ID = 'io.github.kaustubhpatange' 35 | PUBLISH_ARTIFACT_ID = 'autobindings-recyclerview' 36 | PUBLISH_VERSION = versions.library 37 | } 38 | 39 | apply from: "${rootProject.projectDir}/publish.gradle" -------------------------------------------------------------------------------- /autobindings-annotations-recyclerview/src/main/java/com/kpstv/bindings/ClickAnnotation.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.bindings 2 | 3 | import androidx.annotation.IdRes 4 | 5 | /** 6 | * Code example (Kotlin) 7 | * ``` 8 | * @OnClick(...) 9 | * fun onClick(context: Context, item: Data, position: Int): Unit 10 | * ``` 11 | * 12 | * @param setInViewHolder If set to false listener will be generated in onBindViewHolder() 13 | */ 14 | @Target(AnnotationTarget.FUNCTION) 15 | @Retention(AnnotationRetention.SOURCE) 16 | annotation class OnClick( 17 | @IdRes val itemId: Int, 18 | val setInViewHolder: Boolean = true, 19 | val viewType: Int = 0 20 | ) 21 | 22 | /** 23 | * Code example (Kotlin) 24 | * ``` 25 | * @OnLongClick(...) 26 | * fun onClick(context: Context, item: Data, position: Int): Unit 27 | * ``` 28 | * 29 | * @param setInViewHolder If set to false listener will be generated in onBindViewHolder() 30 | */ 31 | @Target(AnnotationTarget.FUNCTION) 32 | @Retention(AnnotationRetention.SOURCE) 33 | annotation class OnLongClick( 34 | @IdRes val itemId: Int, 35 | val setInViewHolder: Boolean = true, 36 | val viewType: Int = 0 37 | ) 38 | -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/utils/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.utils 2 | 3 | import com.squareup.javapoet.TypeName 4 | import javax.lang.model.element.Element 5 | import javax.lang.model.element.ExecutableElement 6 | import javax.lang.model.type.MirroredTypeException 7 | import javax.lang.model.type.TypeMirror 8 | import javax.lang.model.util.ElementFilter 9 | import kotlin.reflect.KClass 10 | 11 | inline fun Element.getElementFromAnnotation(): ExecutableElement? { 12 | ElementFilter.methodsIn(enclosedElements).forEach { 13 | if (it.getAnnotation(T::class.java) != null) 14 | return it 15 | } 16 | return null 17 | } 18 | 19 | inline fun Element.getAnnotationClassValue(f: T.() -> KClass<*>): TypeMirror? = 20 | try { 21 | getAnnotation(T::class.java).f() 22 | throw Exception("Expected to get a MirroredTypeException") 23 | } catch (e: MirroredTypeException) { 24 | e.typeMirror 25 | } 26 | 27 | fun TypeName.simpleName() = toString().substring(toString().lastIndexOf(".") + 1) 28 | 29 | fun TypeMirror.toType() = TypeName.get(this) -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/generators/ViewTypeGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.generators 2 | 3 | import com.kpstv.bindings.ItemViewType 4 | import com.kpstv.processor.utils.Consts 5 | import com.kpstv.processor.utils.getElementFromAnnotation 6 | import com.squareup.javapoet.MethodSpec 7 | import com.squareup.javapoet.TypeName 8 | import javax.lang.model.element.Modifier 9 | import javax.lang.model.element.TypeElement 10 | 11 | object ViewTypeGenerator { 12 | fun generateItemViewType(typeElement: TypeElement): MethodSpec { 13 | val annotation = typeElement.getElementFromAnnotation() 14 | 15 | val methodSpec = MethodSpec.methodBuilder("getItemViewType") 16 | .addParameter(TypeName.INT, Consts.position) 17 | .addAnnotation(Override::class.java) 18 | .addModifiers(Modifier.PUBLIC) 19 | .returns(TypeName.INT) 20 | 21 | if (annotation != null) 22 | methodSpec.addStatement("return ${Consts.className}.${annotation.simpleName}(${Consts.position})") 23 | else methodSpec.addStatement("return 0") 24 | 25 | return methodSpec.build() 26 | } 27 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | -------------------------------------------------------------------------------- /sample-ktx/src/main/java/com/kpstv/sample_ktx/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.sample_ktx 2 | 3 | import java.text.MessageFormat 4 | import kotlin.random.Random 5 | 6 | object Utils { 7 | 8 | private val tags = listOf( 9 | "background", "wallpaper", "halloween", "nature", "food", "christmas", "business", "autumn", "office", "money", "fall", "computer", "love" 10 | ) 11 | 12 | private fun random(start: Int, end: Int): Int { 13 | return start + Random.nextInt(end - start + 1) 14 | } 15 | 16 | fun createRandomImageUrl(): String { 17 | val landscape = Random.nextBoolean() 18 | val endpoint = Random.nextBoolean() 19 | 20 | val width = random(300, 400) 21 | val height = random(200, 300) 22 | 23 | return MessageFormat.format( 24 | if (endpoint) 25 | "https://lorempixel.com/{0}/{1}/" 26 | else 27 | "https://picsum.photos/{0}/{1}/", 28 | if (landscape) width else height, if (landscape) height else width 29 | ) 30 | } 31 | 32 | fun createRandomTag(): List { 33 | val first = Random.nextInt(tags.size - 1) 34 | val second = Random.nextInt(tags.size - 1) 35 | return tags.subList(minOf(first, second), maxOf(first, second)) 36 | } 37 | } -------------------------------------------------------------------------------- /sample-ktx/src/main/java/com/kpstv/sample_ktx/data/Data.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.sample_ktx.data 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.kpstv.bindings.AutoGenerateConverter 6 | import com.kpstv.bindings.AutoGeneratePairConverter 7 | import com.kpstv.bindings.ConverterType 8 | import com.kpstv.samplektx.data.DataDomain 9 | import com.squareup.moshi.JsonClass 10 | import kotlinx.serialization.Serializable 11 | 12 | /** 13 | * POJO class 14 | */ 15 | @Serializable 16 | @Entity(tableName = "table_data") 17 | @JsonClass(generateAdapter = true) 18 | @AutoGeneratePairConverter(keyClass = String::class, using = ConverterType.MOSHI) 19 | data class Data(val name: String, val imageUrl: String, val tags: List) 20 | 21 | fun Data.mapToDomain(): DataDomain { 22 | return DataDomain( 23 | id = 0, 24 | name = name, 25 | imageUrl = imageUrl, 26 | tags = tags 27 | ) 28 | } 29 | 30 | fun List.mapToDomain(): List { 31 | return map { it.mapToDomain() } 32 | } 33 | 34 | enum class Category { 35 | FIRST, SECOND, THIRD 36 | } 37 | 38 | //@Serializable 39 | @Entity(tableName = "table_testdata") 40 | @AutoGenerateConverter(using = ConverterType.KOTLIN_SERIALIZATION) 41 | data class TestDataClass(val name: String, val category: Category) { 42 | @PrimaryKey(autoGenerate = true) 43 | var primaryKey: Int = 0 44 | } -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/generators/ViewHolderGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.generators 2 | 3 | import androidx.annotation.NonNull 4 | import com.kpstv.processor.utils.Consts 5 | import com.squareup.javapoet.* 6 | import javax.lang.model.element.Modifier 7 | 8 | object ViewHolderGenerator { 9 | fun generateOnCreateViewHolder( 10 | viewHolderClassName: ClassName, 11 | codeBlock: CodeBlock 12 | ): MethodSpec { 13 | return MethodSpec.methodBuilder("onCreateViewHolder") 14 | .addModifiers(Modifier.PUBLIC) 15 | .addAnnotation(Override::class.java) 16 | .addAnnotation(NonNull::class.java) 17 | .addParameter(Consts.CLASSNAME_VIEWGROUP, "parent") 18 | .addParameter(TypeName.INT, "viewType") 19 | .returns(viewHolderClassName) 20 | .addCode(codeBlock) 21 | .addStatement("return null") 22 | .build() 23 | } 24 | 25 | fun createViewHolder(viewHolderClassName: ClassName): TypeSpec { 26 | return TypeSpec.classBuilder(viewHolderClassName.simpleName()) 27 | .addModifiers(Modifier.STATIC) 28 | .superclass(Consts.CLASSNAME_VIEWHOLDER) 29 | .addMethod( 30 | MethodSpec.constructorBuilder() 31 | .addModifiers(Modifier.PUBLIC) 32 | .addParameter(Consts.CLASSNAME_VIEW, Consts.view) 33 | .addStatement("super(${Consts.view})") 34 | .build() 35 | ).build() 36 | } 37 | } -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/generators/auto/EnumGenericConverter.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.generators.auto 2 | 3 | import com.kpstv.processor.utils.Consts 4 | import com.kpstv.processor.utils.simpleName 5 | import com.squareup.javapoet.MethodSpec 6 | import com.squareup.javapoet.TypeName 7 | import com.squareup.javapoet.TypeSpec 8 | import javax.lang.model.element.Modifier 9 | 10 | object EnumGenericConverter { 11 | fun generate(typeSpecBuilder: TypeSpec.Builder, typeName: TypeName, packageName: String) { 12 | val className = typeName.toString().replace("$packageName.", "").replace(".","$") 13 | val encodeMethodBuilder = MethodSpec.methodBuilder(Consts.toConvertMethod + className) 14 | .addModifiers(Modifier.PUBLIC, Modifier.STATIC) 15 | .addAnnotation(Consts.CLASSNAME_TYPECONVERTER) 16 | .addParameter(typeName, Consts.converterName) 17 | .returns(String::class.java) 18 | 19 | encodeMethodBuilder.addCode("return ${Consts.converterName}.name();") 20 | 21 | val decodeMethodBuilder = MethodSpec.methodBuilder(Consts.fromConvertMethod + className) 22 | .addModifiers(Modifier.PUBLIC, Modifier.STATIC) 23 | .addAnnotation(Consts.CLASSNAME_TYPECONVERTER) 24 | .addParameter(String::class.java, Consts.converterName) 25 | .returns(typeName) 26 | 27 | decodeMethodBuilder.addCode("return \$T.valueOf(${Consts.converterName});", typeName) 28 | 29 | typeSpecBuilder.addMethod(encodeMethodBuilder.build()) 30 | typeSpecBuilder.addMethod(decodeMethodBuilder.build()) 31 | } 32 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | on: [push] 3 | 4 | jobs: 5 | build: 6 | runs-on: windows-latest 7 | if: "contains(github.event.head_commit.message, '[Released]')" 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Set up JDK 11 11 | uses: actions/setup-java@v1 12 | with: 13 | java-version: 11 14 | - name: Creating local properties file 15 | run: echo signing.keyId=${{ secrets.SIGNING_KEYID }} >> local.properties 16 | - run: echo signing.password=${{ secrets.SIGNING_PASSWORD }} >> local.properties 17 | - run: echo ossrhUsername=${{ secrets.OSSRH_USERNAME }} >> local.properties 18 | - run: echo ossrhPassword=${{ secrets.OSSRH_PASSWORD }} >> local.properties 19 | - name: Creating key.gpg 20 | id: write_file 21 | uses: timheuer/base64-to-file@v1.0.3 22 | with: 23 | fileName: "key.gpg" 24 | encodedString: ${{ secrets.SIGNING_KEY_GPG }} 25 | - run: echo ${{ steps.write_file.outputs.filePath }} 26 | - run: echo signing.secretKeyRingFile=D\:\\a\\_temp\\key.gpg >> local.properties 27 | - run: type local.properties 28 | - run: chmod +x gradlew 29 | - name: Stage annotations 30 | run: ./gradlew autobindings-annotations:publish 31 | - name: Stage compiler 32 | run: ./gradlew autobindings-compiler:publish 33 | - name: Stage Room-noop 34 | run: ./gradlew autobindings-room-noop:publish 35 | - name: Stage library 36 | run: ./gradlew autobindings:publish 37 | - name: Closing repository 38 | run: ./gradlew closeRepository 39 | - name: Release repository 40 | run: ./gradlew releaseRepository 41 | -------------------------------------------------------------------------------- /lint/src/main/java/com/kpstv/lint/IssueRegistry.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.lint 2 | 3 | import com.android.tools.lint.client.api.IssueRegistry 4 | import com.android.tools.lint.client.api.Vendor 5 | import com.android.tools.lint.detector.api.CURRENT_API 6 | import com.android.tools.lint.detector.api.Issue 7 | import com.kpstv.lint.detectors.RecyclerViewDetector 8 | import com.kpstv.lint.detectors.TypeConvertDetector 9 | 10 | class IssueRegistry : IssueRegistry() { 11 | 12 | override val api: Int = CURRENT_API 13 | 14 | override val minApi: Int 15 | get() = api 16 | 17 | override val issues: List 18 | get() = listOf( 19 | RecyclerViewDetector.ISSUE_ON_BIND, 20 | RecyclerViewDetector.ISSUE_RECYCLERVIEW, 21 | RecyclerViewDetector.ISSUE_INCORRECT_BIND, 22 | RecyclerViewDetector.ISSUE_INCORRECT_DIFFCONTENTSAME, 23 | RecyclerViewDetector.ISSUE_INCORRECT_DIFFITEMSAME, 24 | RecyclerViewDetector.ISSUE_NO_DIFFITEMSAME, 25 | RecyclerViewDetector.ISSUE_INCORRECT_ONCLICK, 26 | RecyclerViewDetector.ISSUE_INCORRECT_ONLONGCLICK, 27 | RecyclerViewDetector.ISSUE_NO_DIFFCONTENTSAME, 28 | RecyclerViewDetector.ISSUE_INCORRECT_ITEMVIEWTYPE, 29 | TypeConvertDetector.ISSUE_NO_SERIALIZABLE, 30 | TypeConvertDetector.ISSUE_NO_JSONCLASS, 31 | TypeConvertDetector.ISSUE_NO_INTERFACE, 32 | TypeConvertDetector.ISSUE_WRONG_RETURN_TYPE 33 | ) 34 | 35 | override val vendor: Vendor = Vendor( 36 | vendorName = "Kaustubh Patange", 37 | feedbackUrl = "https://github.com/KaustubhPatange/AutoBindings/issues", 38 | contact = "https://github.com/KaustubhPatange/AutoBindings" 39 | ) 40 | } -------------------------------------------------------------------------------- /autobindings-annotations-recyclerview/src/main/java/com/kpstv/bindings/GlideLoad.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.bindings 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.IdRes 5 | 6 | /** 7 | * Annotate [OnBind] method with this to automatically load image from the url. 8 | * 9 | * Code example (Kotlin) 10 | * ``` 11 | * @GlideLoadArray( 12 | * GlideLoad(R.id.item_big_image, "parameter-name",...) 13 | * ... 14 | * ) 15 | * @OnBind(...) 16 | * fun bind(...): Unit 17 | * ``` 18 | * 19 | * @see [GlideLoad.data] 20 | */ 21 | @Target(AnnotationTarget.FUNCTION) 22 | @Retention(AnnotationRetention.SOURCE) 23 | annotation class GlideLoadArray(vararg val glideLoad: GlideLoad) 24 | 25 | 26 | /** 27 | * @see [GlideLoadArray] 28 | */ 29 | @Target(AnnotationTarget.EXPRESSION) 30 | @Retention(AnnotationRetention.SOURCE) 31 | annotation class GlideLoad( 32 | @IdRes val itemId: Int, 33 | /** 34 | * If suppose `Model` is your data class which is defined below & 35 | * `imageUrl` is the string you want to load. 36 | * ``` 37 | * data class Model(val name: String, val imageUrl: String) 38 | * ``` 39 | * Then set [data] as, 40 | * ``` 41 | * data = imageUrl 42 | * ``` 43 | * 44 | * For nested types you can put period and go on. 45 | */ 46 | val data: String, 47 | @DrawableRes 48 | val placeHolderRes: Int = -1, 49 | @DrawableRes 50 | val errorRes: Int = -1, 51 | val cachingStrategyImage: ImageCacheStrategyType = ImageCacheStrategyType.ALL, 52 | val transformationType: ImageTransformationType = ImageTransformationType.NONE 53 | ) 54 | 55 | enum class ImageCacheStrategyType { 56 | ALL, NONE, AUTOMATIC, DATA, RESOURCE 57 | } 58 | 59 | enum class ImageTransformationType { 60 | NONE, CENTER_CROP, CENTER_INSIDE, CIRCLE_CROP, FIT_CENTER 61 | } -------------------------------------------------------------------------------- /sample-ktx/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /lint/src/test/java/com/kpstv/lint/CorrectRecyclerViewListImplementationTests.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.lint 2 | 3 | import com.android.tools.lint.checks.infrastructure.TestFiles 4 | import com.android.tools.lint.checks.infrastructure.TestLintTask 5 | import com.kpstv.lint.detectors.RecyclerViewDetector 6 | import com.kpstv.lint.utils.Stubs 7 | import org.junit.Test 8 | 9 | class CorrectRecyclerViewListImplementationTests { 10 | @Test 11 | fun correctImplementationOfDiffCallbackTest() { 12 | val stubFile = TestFiles.kotlin( 13 | """ 14 | package com.kpstv.lint 15 | 16 | import com.kpstv.bindings.DiffItemSame 17 | import com.kpstv.bindings.DiffContentSame 18 | import com.kpstv.bindings.RecyclerViewListAdapter 19 | import kotlin.reflect.KClass 20 | 21 | @RecyclerViewListAdapter 22 | class TestAdapter { 23 | @DiffItemSame 24 | fun diffItemSame(o: KClass<*>, n: KClass<*>): Boolean = false 25 | 26 | @DiffContentSame 27 | fun diffContentSame(o: KClass<*>, n: KClass<*>): Boolean = false 28 | } 29 | """ 30 | ).indented() 31 | TestLintTask.lint().files( 32 | Stubs.DIFFCONTENTSAME_ANNOTATION, 33 | Stubs.DIFFITEMSAME_ANNOTATION, 34 | Stubs.RECYCLERVIEWLIST_ANNOTATION, 35 | stubFile 36 | ).issues( 37 | RecyclerViewDetector.ISSUE_NO_DIFFITEMSAME, 38 | RecyclerViewDetector.ISSUE_NO_DIFFCONTENTSAME, 39 | RecyclerViewDetector.ISSUE_INCORRECT_DIFFITEMSAME, 40 | RecyclerViewDetector.ISSUE_INCORRECT_DIFFCONTENTSAME 41 | ) 42 | .run() 43 | .expect("No warnings.") 44 | } 45 | } -------------------------------------------------------------------------------- /lint/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'kotlin' 4 | id 'com.android.lint' 5 | } 6 | 7 | lintOptions { 8 | htmlReport true 9 | htmlOutput file("lint-report.html") 10 | textReport true 11 | absolutePaths false 12 | } 13 | 14 | dependencies { 15 | compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 16 | 17 | compileOnly project(":autobindings-annotations-recyclerview") 18 | compileOnly project(":autobindings-annotations-room") 19 | compileOnly project(":autobindings-annotations-sqldelight") 20 | 21 | compileOnly "com.android.tools.lint:lint-api:$lintVersion" 22 | compileOnly "com.android.tools.lint:lint-checks:$lintVersion" 23 | 24 | testImplementation "junit:junit:4.13.2" 25 | 26 | testImplementation "com.android.tools.lint:lint:$lintVersion" 27 | testImplementation "com.android.tools.lint:lint-tests:$lintVersion" 28 | 29 | compileOnly 'androidx.annotation:annotation:1.1.0' 30 | } 31 | 32 | sourceCompatibility = "1.8" 33 | targetCompatibility = "1.8" 34 | 35 | jar{ 36 | manifest{ 37 | attributes 'Lint-Registry-V2': 'com.kpstv.lint.IssueRegistry' 38 | } 39 | } 40 | /* 41 | apply plugin: 'java-library' 42 | apply plugin: 'kotlin' 43 | 44 | dependencies { 45 | compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 46 | 47 | compileOnly "com.android.tools.lint:lint-api:27.0.1" 48 | compileOnly "com.android.tools.lint:lint-checks:27.0.1" 49 | 50 | testImplementation "com.android.tools.lint:lint:27.0.1" 51 | testImplementation "com.android.tools.lint:lint-tests:27.0.1" 52 | 53 | compileOnly project(":autobindings-annotations") 54 | compileOnly 'androidx.annotation:annotation:1.1.0' 55 | } 56 | 57 | sourceCompatibility = "1.7" 58 | targetCompatibility = "1.7" 59 | 60 | jar{ 61 | manifest{ 62 | attributes 'Lint-Registry-V2': 'com.kpstv.lint.IssueRegistry' 63 | } 64 | }*/ 65 | -------------------------------------------------------------------------------- /autobindings-annotations-room/src/main/java/com/kpstv/bindings/AutoGenerateConverter.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.bindings 2 | 3 | import kotlin.reflect.KClass 4 | 5 | /** 6 | * Automatically generate [TypeConverter] for a Room Database. 7 | * 8 | * @see Documentation 9 | */ 10 | @Target(AnnotationTarget.CLASS) 11 | @Retention(AnnotationRetention.SOURCE) 12 | annotation class AutoGenerateConverter(val using: ConverterType) 13 | 14 | /** 15 | * Automatically generate List [TypeConverter] for a Room Database. 16 | * 17 | * @see Documentation 18 | */ 19 | @Target(AnnotationTarget.CLASS) 20 | @Retention(AnnotationRetention.SOURCE) 21 | annotation class AutoGenerateListConverter(val using: ConverterType) 22 | 23 | /** 24 | * Automatically generate Map [TypeConverter] for a Room Database. 25 | * Parameter [keyClass] is your K & the annotated data class is your V in Map 26 | * 27 | * @see Documentation 28 | */ 29 | @Target(AnnotationTarget.CLASS) 30 | @Retention(AnnotationRetention.SOURCE) 31 | annotation class AutoGenerateMapConverter(val keyClass: KClass<*>, val using: ConverterType) 32 | 33 | /** 34 | * Automatically generate Pair [TypeConverter] for a Room Database. 35 | * Parameter [keyClass] is your K & the annotated data class is your V in Pair 36 | * 37 | * @see Documentation 38 | */ 39 | @Target(AnnotationTarget.CLASS) 40 | @Retention(AnnotationRetention.SOURCE) 41 | annotation class AutoGeneratePairConverter(val keyClass: KClass<*>, val using: ConverterType) 42 | 43 | /** 44 | * If marked to field like Enum, the processor will not generate the converter for that element. 45 | * 46 | * @see Documentation 47 | */ 48 | @Target(AnnotationTarget.FIELD) 49 | @Retention(AnnotationRetention.SOURCE) 50 | annotation class IgnoreConverter -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/generators/DiffUtilGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.generators 2 | 3 | import com.kpstv.bindings.DiffContentSame 4 | import com.kpstv.bindings.DiffItemSame 5 | import com.kpstv.processor.utils.Consts 6 | import com.kpstv.processor.utils.getElementFromAnnotation 7 | import com.squareup.javapoet.MethodSpec 8 | import com.squareup.javapoet.ParameterizedTypeName 9 | import com.squareup.javapoet.TypeName 10 | import com.squareup.javapoet.TypeSpec 11 | import javax.lang.model.element.Modifier 12 | import javax.lang.model.element.TypeElement 13 | 14 | object DiffUtilGenerator { 15 | fun generateDiffUtil(typeElement: TypeElement, dataClassTypeName: TypeName): TypeSpec { 16 | 17 | val diffItemSame = typeElement.getElementFromAnnotation() 18 | val diffContentSame = typeElement.getElementFromAnnotation() 19 | 20 | return TypeSpec.anonymousClassBuilder("") 21 | .addSuperinterface( 22 | ParameterizedTypeName.get(Consts.CLASSNAME_DIFFUTIL, dataClassTypeName) 23 | ) 24 | .addMethod( 25 | MethodSpec.methodBuilder("areItemsTheSame") 26 | .returns(TypeName.BOOLEAN) 27 | .addModifiers(Modifier.PUBLIC) 28 | .addAnnotation(Override::class.java) 29 | .addParameter(dataClassTypeName, "oldItem") 30 | .addParameter(dataClassTypeName, "newItem") 31 | .addStatement("return ${Consts.className}.${diffItemSame?.simpleName}(oldItem, newItem)") 32 | .build() 33 | ) 34 | .addMethod( 35 | MethodSpec.methodBuilder("areContentsTheSame") 36 | .returns(TypeName.BOOLEAN) 37 | .addModifiers(Modifier.PUBLIC) 38 | .addAnnotation(Override::class.java) 39 | .addParameter(dataClassTypeName, "oldItem") 40 | .addParameter(dataClassTypeName, "newItem") 41 | .addStatement("return ${Consts.className}.${diffContentSame?.simpleName}(oldItem, newItem)") 42 | .build() 43 | ) 44 | .build() 45 | } 46 | } -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/generators/auto/SQLDelightAdapterProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.generators.auto 2 | 3 | import androidx.annotation.NonNull 4 | import com.kpstv.bindings.ConverterType 5 | import com.kpstv.processor.abstract.BaseAutoGenerator 6 | import com.kpstv.processor.utils.AutoGeneratorType 7 | import com.kpstv.processor.utils.Consts 8 | import com.squareup.javapoet.* 9 | import javax.lang.model.element.Modifier 10 | 11 | class SQLDelightAdapterProcessor( 12 | override val serializerType: ConverterType, 13 | private val adapterName: String, 14 | override val baseDataType: TypeName, 15 | ) : BaseAutoGenerator() { 16 | 17 | override val converterType = AutoGeneratorType.SQLDelight 18 | 19 | private val parameterizedType = 20 | ParameterizedTypeName.get(Consts.CLASSNAME_COLUMNADAPTER, baseDataType, ClassName.get(String::class.java)) 21 | 22 | override val typeSpecBuilder: TypeSpec.Builder = TypeSpec.anonymousClassBuilder("") 23 | .addSuperinterface(parameterizedType) 24 | 25 | override fun encodeBuilder(): MethodSpec.Builder { 26 | return MethodSpec.methodBuilder(Consts.encodeMethod) 27 | .addModifiers(Modifier.PUBLIC) 28 | .addAnnotation(Override::class.java) 29 | .addAnnotation(NonNull::class.java) 30 | .addParameter(ParameterSpec.builder(baseDataType, Consts.converterName) 31 | .addAnnotation(NonNull::class.java).build()) 32 | .returns(String::class.java) 33 | } 34 | 35 | override fun decodeBuilder(): MethodSpec.Builder { 36 | return MethodSpec.methodBuilder(Consts.decodeMethod) 37 | .addModifiers(Modifier.PUBLIC) 38 | .addAnnotation(Override::class.java) 39 | .addParameter(String::class.java, Consts.converterName) 40 | .returns(baseDataType) 41 | } 42 | 43 | fun generateMethod(): MethodSpec { 44 | create() 45 | return MethodSpec.methodBuilder(adapterName) 46 | .addModifiers(Modifier.PUBLIC) 47 | .returns(parameterizedType) 48 | .addAnnotation(Override::class.java) 49 | .addAnnotation(NonNull::class.java) 50 | .addCode("return \$L;", typeSpecBuilder.build()) 51 | .build() 52 | } 53 | } -------------------------------------------------------------------------------- /sample-ktx/src/main/res/layout/item_small_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 19 | 20 | 31 | 32 | 43 | 44 | 54 | 55 | -------------------------------------------------------------------------------- /lint/src/test/java/com/kpstv/lint/CorrectAutogenerateConverterTests.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.lint 2 | 3 | import com.android.tools.lint.checks.infrastructure.LintDetectorTest 4 | import com.android.tools.lint.checks.infrastructure.TestLintTask 5 | import com.kpstv.lint.detectors.TypeConvertDetector 6 | import com.kpstv.lint.utils.Stubs 7 | import org.junit.Test 8 | 9 | class CorrectAutogenerateConverterTests { 10 | @Test 11 | fun correctSerializationTest() { 12 | val stubFile = LintDetectorTest.kotlin( 13 | """ 14 | package com.kpstv.lint 15 | import com.kpstv.bindings.AutoGenerateConverter 16 | import com.kpstv.bindings.ConverterType 17 | import kotlinx.serialization.Serializable 18 | 19 | @Serializable 20 | @AutoGenerateConverter(using = ConverterType.KOTLIN_SERIALIZATION) 21 | data class User(val name: String) 22 | """ 23 | ).indented() 24 | TestLintTask.lint().files( 25 | Stubs.CLASS_CONVERTER_TYPE, 26 | Stubs.CLASS_KOTLIN_SERIALIZABLE, 27 | Stubs.LISTCONVERTER_ANNOTATIONS, 28 | Stubs.CONVERTER_ANNOTATIONS, 29 | stubFile 30 | ) 31 | .issues(TypeConvertDetector.ISSUE_NO_SERIALIZABLE) 32 | .run() 33 | .expect( 34 | "No warnings." 35 | ) 36 | } 37 | 38 | @Test 39 | fun correctJsonClassTest() { 40 | val stubFile = LintDetectorTest.kotlin( 41 | """ 42 | package com.kpstv.lint 43 | import com.kpstv.bindings.AutoGenerateListConverter 44 | import com.kpstv.bindings.ConverterType 45 | import com.squareup.moshi.JsonClass 46 | 47 | @JsonClass(generateAdapter = true) 48 | @AutoGenerateListConverter(using = ConverterType.MOSHI) 49 | data class User(val name: String) 50 | """ 51 | ).indented() 52 | TestLintTask.lint().files( 53 | Stubs.CLASS_CONVERTER_TYPE, 54 | Stubs.CLASS_KOTLIN_SERIALIZABLE, 55 | Stubs.LISTCONVERTER_ANNOTATIONS, 56 | Stubs.CONVERTER_ANNOTATIONS, 57 | Stubs.CLASS_JSONCLASS, 58 | stubFile 59 | ) 60 | .issues(TypeConvertDetector.ISSUE_NO_JSONCLASS) 61 | .run() 62 | .expect( 63 | "No warnings." 64 | ) 65 | } 66 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/generators/auto/TypeConverterProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.generators.auto 2 | 3 | import com.kpstv.bindings.ConverterType 4 | import com.kpstv.processor.abstract.BaseAutoGenerator 5 | import com.kpstv.processor.utils.AutoGeneratorDataType 6 | import com.kpstv.processor.utils.AutoGeneratorType 7 | import com.kpstv.processor.utils.Consts 8 | import com.squareup.javapoet.* 9 | import javax.lang.model.element.Modifier 10 | 11 | class TypeConverterProcessor( 12 | override val typeSpecBuilder: TypeSpec.Builder, 13 | override val serializerType: ConverterType, 14 | private val firstClassType: ClassName, 15 | private val enumList: List, 16 | generatorDataType: AutoGeneratorDataType, 17 | secondClassType: TypeName? = null, 18 | ) : BaseAutoGenerator() { 19 | 20 | override val converterType = AutoGeneratorType.ROOM 21 | 22 | override val baseDataType: TypeName = when(generatorDataType) { 23 | AutoGeneratorDataType.DATA -> firstClassType 24 | AutoGeneratorDataType.LIST -> ParameterizedTypeName.get(Consts.CLASSNAME_LIST, firstClassType) 25 | AutoGeneratorDataType.MAP -> ParameterizedTypeName.get(Consts.CLASSNAME_MAP, secondClassType, firstClassType) 26 | AutoGeneratorDataType.PAIR -> ParameterizedTypeName.get(Consts.CLASSNAME_PAIR, secondClassType, firstClassType) 27 | } 28 | 29 | override fun encodeBuilder(): MethodSpec.Builder { 30 | return MethodSpec.methodBuilder(Consts.toConvertMethod + firstClassType.simpleName()) 31 | .addModifiers(Modifier.PUBLIC, Modifier.STATIC) 32 | .addAnnotation(Consts.CLASSNAME_TYPECONVERTER) 33 | .addAnnotation(Consts.CLASSNAME_NULLABLE) 34 | .addParameter(baseDataType, Consts.converterName) 35 | .returns(String::class.java) 36 | } 37 | 38 | override fun decodeBuilder(): MethodSpec.Builder { 39 | return MethodSpec.methodBuilder(Consts.fromConvertMethod + firstClassType.simpleName()) 40 | .addModifiers(Modifier.PUBLIC, Modifier.STATIC) 41 | .addAnnotation(Consts.CLASSNAME_TYPECONVERTER) 42 | .addAnnotation(Consts.CLASSNAME_NULLABLE) 43 | .addParameter(String::class.java, Consts.converterName) 44 | .returns(baseDataType) 45 | } 46 | 47 | override fun create() { 48 | super.create() 49 | for(enum in enumList) { 50 | EnumGenericConverter.generate(typeSpecBuilder, enum, firstClassType.packageName()) 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /lint/src/test/java/com/kpstv/lint/CorrectRecyclerViewImplementationTests.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.lint 2 | 3 | import com.android.tools.lint.checks.infrastructure.TestFiles 4 | import com.android.tools.lint.checks.infrastructure.TestLintTask 5 | import com.kpstv.lint.detectors.RecyclerViewDetector 6 | import com.kpstv.lint.utils.Stubs 7 | import org.junit.Test 8 | 9 | class CorrectRecyclerViewImplementationTests { 10 | 11 | @Test 12 | fun correctImplementationOfRecyclerViewTest() { 13 | val stubFile = TestFiles.kotlin( 14 | """ 15 | package com.kpstv.lint 16 | 17 | import com.kpstv.bindings.RecyclerViewAdapter 18 | import com.kpstv.bindings.OnBind 19 | import kotlin.reflect.KClass 20 | import android.view.View 21 | import com.kpstv.bindings.OnClick 22 | import com.kpstv.bindings.OnLongClick 23 | import com.kpstv.bindings.ItemViewType 24 | import android.content.Context 25 | data class TestClass(val name: String) 26 | @RecyclerViewAdapter(TestClass::class.java) 27 | class TestAdapter { 28 | @OnBind(layoutId = 12) 29 | fun bind(v: View, i: TestClass, p: Int) {} 30 | 31 | @OnClick(2) 32 | fun onClick(c: Context, i: KClass<*>, p: Int) {} 33 | 34 | @OnLongClick(2) 35 | fun onLongClick(c: Context, i: KClass<*>, p: Int) {} 36 | 37 | @ItemViewType 38 | fun viewType(p: Int): Int = 0 39 | } 40 | """ 41 | ).indented() 42 | TestLintTask.lint().files( 43 | Stubs.CLASS_CONTEXT, 44 | Stubs.CLASS_VIEW, 45 | Stubs.RECYCLERVIEW_ANNOTATION, 46 | Stubs.ONBIND_ANNOTATION, 47 | Stubs.ONCLICK_ANNOTATION, 48 | Stubs.ONLONGCLICK_ANNOTATION, 49 | Stubs.ITEMVIEWTYPE_ANNOTATION, 50 | stubFile 51 | ) 52 | .issues( 53 | RecyclerViewDetector.ISSUE_INCORRECT_BIND, 54 | RecyclerViewDetector.ISSUE_RECYCLERVIEW, 55 | RecyclerViewDetector.ISSUE_ON_BIND, 56 | RecyclerViewDetector.ISSUE_INCORRECT_ONCLICK, 57 | RecyclerViewDetector.ISSUE_INCORRECT_ONLONGCLICK, 58 | RecyclerViewDetector.ISSUE_INCORRECT_ITEMVIEWTYPE 59 | ) 60 | .allowCompilationErrors() 61 | .run() 62 | .expect("No warnings.") 63 | } 64 | } -------------------------------------------------------------------------------- /lint/src/test/java/com/kpstv/lint/WrongAutogenerateConverterTests.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.lint 2 | 3 | import com.android.tools.lint.checks.infrastructure.LintDetectorTest.kotlin 4 | import com.android.tools.lint.checks.infrastructure.TestLintTask.lint 5 | import com.kpstv.lint.detectors.TypeConvertDetector 6 | import com.kpstv.lint.utils.Stubs 7 | import org.junit.Test 8 | 9 | class WrongAutogenerateConverterTests { 10 | @Test 11 | fun wrongSerializationTest() { 12 | val stubFile = kotlin( 13 | """ 14 | package com.kpstv.lint 15 | import com.kpstv.bindings.AutoGenerateConverter 16 | import com.kpstv.bindings.ConverterType 17 | 18 | @AutoGenerateConverter(using = ConverterType.KOTLIN_SERIALIZATION) 19 | data class User(val name: String) 20 | """ 21 | ).indented() 22 | lint().files( 23 | Stubs.CLASS_CONVERTER_TYPE, 24 | Stubs.CLASS_KOTLIN_SERIALIZABLE, 25 | Stubs.LISTCONVERTER_ANNOTATIONS, 26 | Stubs.CONVERTER_ANNOTATIONS, 27 | stubFile 28 | ) 29 | .issues(TypeConvertDetector.ISSUE_NO_SERIALIZABLE) 30 | .run() 31 | .expect( 32 | """ 33 | src/com/kpstv/lint/User.kt:5: Warning: Class must be annotated with @Serializable. [kotlinSerialization] 34 | @AutoGenerateConverter(using = ConverterType.KOTLIN_SERIALIZATION) 35 | ^ 36 | 0 errors, 1 warnings 37 | """.trimIndent() 38 | ) 39 | } 40 | 41 | @Test 42 | fun wrongJsonClassTest() { 43 | val stubFile = kotlin( 44 | """ 45 | package com.kpstv.lint 46 | import com.kpstv.bindings.AutoGenerateListConverter 47 | import com.kpstv.bindings.ConverterType 48 | 49 | @AutoGenerateListConverter(using = ConverterType.MOSHI) 50 | data class User(val name: String) 51 | """ 52 | ).indented() 53 | lint().files( 54 | Stubs.CLASS_CONVERTER_TYPE, 55 | Stubs.CLASS_KOTLIN_SERIALIZABLE, 56 | Stubs.LISTCONVERTER_ANNOTATIONS, 57 | Stubs.CONVERTER_ANNOTATIONS, 58 | stubFile 59 | ) 60 | .issues(TypeConvertDetector.ISSUE_NO_JSONCLASS) 61 | .run() 62 | .expect( 63 | """ 64 | src/com/kpstv/lint/User.kt:5: Warning: Class must be annotated with @JsonClass(generateAdapter = true). [noJsonClass] 65 | @AutoGenerateListConverter(using = ConverterType.MOSHI) 66 | ^ 67 | 0 errors, 1 warnings 68 | """.trimIndent() 69 | ) 70 | } 71 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AutoBindings 2 | 3 | ![build](https://github.com/KaustubhPatange/AutoBindings/workflows/build/badge.svg) 4 | ![Maven Central](https://img.shields.io/maven-central/v/io.github.kaustubhpatange/autobindings-compiler) 5 | 6 | ### This project is _Discontinued_ as such logic for creating `RecyclerView` adapters, `TypeConverter`s for Room or SQLDelight can easily be abstracted into an abstract class. No need to use code generation. 7 | 8 | ___ 9 | 10 | **AutoBindings** is a set of annotations which will make Android development easier by eliminating boilerplate codes. 11 | 12 | ## Usage 13 | 14 | Library currently supports 15 | 16 | - [Generation of RecyclerView adapters](https://github.com/KaustubhPatange/AutoBindings/wiki/Adapter-Generation) 17 | - [Generation of `TypeConverters` for Room](https://github.com/KaustubhPatange/AutoBindings/wiki/TypeConverter-Generation) 18 | - [Generation of `ColumnAdapters` for custom types in SQLDelight](https://github.com/KaustubhPatange/AutoBindings/wiki/ColumnAdapter-Generation) 19 | - [Use of `TypeConverters` for general purpose](https://github.com/KaustubhPatange/AutoBindings/wiki/Generic-use-of-TypeConverters) 20 | 21 | You can find some set of extra compiler options [here](https://github.com/KaustubhPatange/AutoBindings/wiki/Compiler-Options). 22 | 23 | ## Download 24 | 25 | ```groovy 26 | implementation 'io.github.kaustubhpatange:autobindings-recyclerview:' // For Recyclerview bindings 27 | implementation 'io.github.kaustubhpatange:autobindings-room:' // For Room bindings 28 | implementation 'io.github.kaustubhpatange:autobindings-sqldelight:' // For SQLDelight bindings 29 | 30 | implementation "io.github.kaustubhpatange:autobindings-room-noop:" // For general use of typeconverters if you don't depend on Room 31 | 32 | // Kotlin 33 | apply plugin: 'kotlin-kapt' // at top of your module build.gradle file 34 | kapt 'io.github.kaustubhpatange:autobindings-compiler:' 35 | // Java 36 | annotationProcessor 'io.github.kaustubhpatange:autobindings-compiler:' 37 | ``` 38 | 39 | ## Resources 40 | 41 | - **Medium article(s)** 42 | - [Auto-Generate TypeConverters for entity classes: Room](https://medium.com/@developerkp16/auto-generate-typeconverters-for-entity-classes-room-1b40a793c146) 43 | - [“Custom column types” in SQLDelight: Android](https://developerkp16.medium.com/custom-column-types-in-sqldelight-android-a6f166635464) 44 | 45 | ## License 46 | 47 | - [The Apache License Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) 48 | 49 | ``` 50 | Copyright 2020 Kaustubh Patange 51 | 52 | Licensed under the Apache License, Version 2.0 (the "License"); 53 | you may not use this file except in compliance with the License. 54 | You may obtain a copy of the License at 55 | 56 | https://www.apache.org/licenses/LICENSE-2.0 57 | 58 | Unless required by applicable law or agreed to in writing, software 59 | distributed under the License is distributed on an "AS IS" BASIS, 60 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 61 | See the License for the specific language governing permissions and 62 | limitations under the License. 63 | ``` 64 | -------------------------------------------------------------------------------- /lint/src/test/java/com/kpstv/lint/WrongAutoGenerateSQLAdapterTests.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.lint 2 | 3 | import com.android.tools.lint.checks.infrastructure.LintDetectorTest 4 | import com.android.tools.lint.checks.infrastructure.TestLintTask 5 | import com.kpstv.lint.detectors.TypeConvertDetector 6 | import com.kpstv.lint.utils.Stubs 7 | import org.junit.Test 8 | 9 | class WrongAutoGenerateSQLAdapterTests { 10 | @Test 11 | fun wrongAutoGenerateSQLAdapterInterfaceTest() { 12 | val stubFile = LintDetectorTest.kotlin( 13 | """ 14 | package com.kpstv.lint 15 | import com.kpstv.bindings.AutoGenerateSQLDelightAdapters 16 | import com.squareup.sqldelight.ColumnAdapter 17 | 18 | data class User(val name: String, val age: Int) 19 | 20 | @AutoGenerateSQLDelightAdapters(using = ConverterType.KOTLIN_SERIALIZATION) 21 | abstract class SQLDelightAdapters { 22 | abstract fun dataConverter(): ColumnAdapter 23 | } 24 | """ 25 | ).indented() 26 | TestLintTask.lint().files( 27 | Stubs.CLASS_CONVERTER_TYPE, 28 | Stubs.COLUMNADAPTER_ANNOTATIONS, 29 | Stubs.CLASS_COLUMNADAPTER, 30 | stubFile 31 | ) 32 | .issues(TypeConvertDetector.ISSUE_NO_INTERFACE) 33 | .run() 34 | .expect( 35 | """ 36 | src/com/kpstv/lint/User.kt:7: Error: Class must be an interface [delightNoInterface] 37 | @AutoGenerateSQLDelightAdapters(using = ConverterType.KOTLIN_SERIALIZATION) 38 | ^ 39 | 1 errors, 0 warnings 40 | """.trimIndent() 41 | ) 42 | } 43 | 44 | @Test 45 | fun wrongAutoGenerateSQLDelightAdapterInvalidReturnTypeTest() { 46 | val stubFile = LintDetectorTest.kotlin( 47 | """ 48 | package com.kpstv.lint 49 | import com.kpstv.bindings.AutoGenerateSQLDelightAdapters 50 | import com.squareup.sqldelight.ColumnAdapter 51 | 52 | data class User(val name: String, val age: Int) 53 | 54 | @AutoGenerateSQLDelightAdapters(using = ConverterType.KOTLIN_SERIALIZATION) 55 | interface SQLDelightAdapters { 56 | fun dataConverter(): ColumnAdapter, Int> 57 | } 58 | """ 59 | ).indented() 60 | TestLintTask.lint().files( 61 | Stubs.CLASS_CONVERTER_TYPE, 62 | Stubs.COLUMNADAPTER_ANNOTATIONS, 63 | Stubs.CLASS_COLUMNADAPTER, 64 | stubFile 65 | ) 66 | .issues(TypeConvertDetector.ISSUE_WRONG_RETURN_TYPE) 67 | .run() 68 | .expect( 69 | """ 70 | src/com/kpstv/lint/User.kt:9: Error: Return type of function must be of type ColumnAdapter<*, String> [delightWrongReturnType] 71 | fun dataConverter(): ColumnAdapter, Int> 72 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 73 | 1 errors, 0 warnings 74 | """.trimIndent() 75 | ) 76 | } 77 | } -------------------------------------------------------------------------------- /sample-ktx/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | apply plugin: 'kotlinx-serialization' 6 | apply plugin: 'com.squareup.sqldelight' 7 | 8 | android { 9 | compileSdkVersion 30 10 | buildToolsVersion "30.0.0" 11 | 12 | defaultConfig { 13 | applicationId "com.kpstv.sample_ktx" 14 | minSdkVersion 21 15 | targetSdkVersion 30 16 | versionCode 1 17 | versionName "1.0" 18 | 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | } 21 | 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | 29 | buildFeatures { 30 | viewBinding = true 31 | } 32 | 33 | compileOptions { 34 | sourceCompatibility = JavaVersion.VERSION_1_8 35 | targetCompatibility = JavaVersion.VERSION_1_8 36 | } 37 | packagingOptions { 38 | exclude 'META-INF/DEPENDENCIES' 39 | exclude 'META-INF/LICENSE' 40 | exclude 'META-INF/LICENSE.txt' 41 | exclude 'META-INF/license.txt' 42 | exclude 'META-INF/NOTICE' 43 | exclude 'META-INF/NOTICE.txt' 44 | exclude 'META-INF/notice.txt' 45 | exclude 'META-INF/ASL2.0' 46 | exclude("META-INF/*.kotlin_module") 47 | } 48 | } 49 | 50 | dependencies { 51 | implementation fileTree(dir: "libs", include: ["*.jar"]) 52 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 53 | implementation 'androidx.core:core-ktx:1.3.1' 54 | implementation 'androidx.appcompat:appcompat:1.2.0' 55 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 56 | testImplementation 'junit:junit:4.12' 57 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 58 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 59 | 60 | implementation 'com.google.android.material:material:1.2.0-beta01' 61 | 62 | // Custom Serializers (for testing) 63 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0" 64 | implementation "com.squareup.moshi:moshi-kotlin-codegen:1.11.0" 65 | implementation 'com.google.code.gson:gson:2.8.6' 66 | 67 | // SQLDelight Driver (for testing) 68 | implementation "com.squareup.sqldelight:android-driver:1.4.3" 69 | 70 | // Room (for testing) 71 | def room_version = "2.2.5" 72 | implementation "androidx.room:room-runtime:$room_version" 73 | kapt "androidx.room:room-compiler:$room_version" 74 | implementation "androidx.room:room-ktx:$room_version" 75 | 76 | // AutoBindings 77 | implementation project (":autobindings-recyclerview") 78 | implementation project (":autobindings-room") 79 | implementation project (":autobindings-sqldelight") 80 | kapt project (':compiler') 81 | 82 | //def auto_bindings = versions.library 83 | //implementation project(":autobindings-annotations") 84 | //kapt "io.github.kaustubhpatange:autobindings-compiler:$auto_bindings" 85 | 86 | implementation 'com.github.bumptech.glide:glide:4.11.0' 87 | } 88 | -------------------------------------------------------------------------------- /lint/src/test/java/com/kpstv/lint/WrongRecyclerViewListImplementationTests.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.lint 2 | 3 | import com.android.tools.lint.checks.infrastructure.TestFiles 4 | import com.android.tools.lint.checks.infrastructure.TestLintTask 5 | import com.kpstv.lint.detectors.RecyclerViewDetector 6 | import com.kpstv.lint.utils.Stubs 7 | import org.junit.Test 8 | 9 | class WrongRecyclerViewListImplementationTests { 10 | @Test 11 | fun noRecyclerViewDiffCallbackImplementationTest() { 12 | val stubFile = TestFiles.kotlin( 13 | """ 14 | package com.kpstv.lint 15 | 16 | import com.kpstv.bindings.RecyclerViewListAdapter 17 | 18 | @RecyclerViewListAdapter 19 | class TestAdapter { 20 | } 21 | """ 22 | ).indented() 23 | TestLintTask.lint().files( 24 | Stubs.RECYCLERVIEWLIST_ANNOTATION, 25 | stubFile 26 | ).issues(RecyclerViewDetector.ISSUE_NO_DIFFITEMSAME, RecyclerViewDetector.ISSUE_NO_DIFFCONTENTSAME) 27 | .run() 28 | .expect( 29 | """ 30 | src/com/kpstv/lint/TestAdapter.kt:6: Error: @DiffContentSame method is not implemented. [noDiffContentSameCallback] 31 | class TestAdapter { 32 | ~~~~~~~~~~~ 33 | src/com/kpstv/lint/TestAdapter.kt:6: Error: @DiffItemSame method is not implemented. [noDiffItemSameCallback] 34 | class TestAdapter { 35 | ~~~~~~~~~~~ 36 | 2 errors, 0 warnings 37 | """.trimIndent() 38 | ) 39 | } 40 | 41 | @Test 42 | fun incorrectRecyclerViewDiffCallbackImplementationTest() { 43 | val stubFile = TestFiles.kotlin( 44 | """ 45 | package com.kpstv.lint 46 | 47 | import com.kpstv.bindings.DiffItemSame 48 | import com.kpstv.bindings.DiffContentSame 49 | import com.kpstv.bindings.RecyclerViewListAdapter 50 | 51 | @RecyclerViewListAdapter 52 | class TestAdapter { 53 | @DiffItemSame 54 | fun diffItemSame() {} 55 | 56 | @DiffContentSame 57 | fun diffContentSame() {} 58 | } 59 | """ 60 | ).indented() 61 | TestLintTask.lint().files( 62 | Stubs.DIFFCONTENTSAME_ANNOTATION, 63 | Stubs.DIFFITEMSAME_ANNOTATION, 64 | Stubs.RECYCLERVIEWLIST_ANNOTATION, 65 | stubFile 66 | ).issues( 67 | RecyclerViewDetector.ISSUE_INCORRECT_DIFFCONTENTSAME, 68 | RecyclerViewDetector.ISSUE_INCORRECT_DIFFITEMSAME 69 | ) 70 | .run() 71 | .expect( 72 | """ 73 | src/com/kpstv/lint/TestAdapter.kt:12: Error: @DiffContentSame method is implemented incorrectly. [incorrectDiffItemContentCallback] 74 | @DiffContentSame 75 | ^ 76 | src/com/kpstv/lint/TestAdapter.kt:9: Error: @DiffItemSame method is implemented incorrectly. [incorrectDiffItemSameCallback] 77 | @DiffItemSame 78 | ^ 79 | 2 errors, 0 warnings 80 | """.trimIndent() 81 | ) 82 | } 83 | } -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.utils 2 | 3 | import com.kpstv.bindings.ImageTransformationType 4 | import com.kpstv.processor.utils.Consts.AUTOBINDINGS_INCREMENTAL 5 | import com.squareup.javapoet.JavaFile 6 | import com.squareup.javapoet.TypeSpec 7 | import java.io.File 8 | import java.io.IOException 9 | import java.lang.StringBuilder 10 | import java.nio.file.FileAlreadyExistsException 11 | import javax.annotation.processing.FilerException 12 | import javax.annotation.processing.ProcessingEnvironment 13 | import javax.lang.model.element.TypeElement 14 | import javax.lang.model.type.DeclaredType 15 | import javax.lang.model.type.TypeMirror 16 | import javax.lang.model.util.Types 17 | import javax.tools.Diagnostic 18 | import javax.tools.JavaFileManager 19 | import javax.tools.StandardLocation 20 | 21 | object Utils { 22 | fun write( 23 | packageName: String, 24 | typeSpec: TypeSpec, 25 | processingEnv: ProcessingEnvironment, 26 | typeElement: TypeElement? = null 27 | ) { 28 | try { 29 | JavaFile.builder( 30 | packageName, 31 | typeSpec 32 | ) 33 | .build() 34 | .writeTo(processingEnv.filer) 35 | } catch (e: FilerException) { 36 | processingEnv.messager.printMessage( 37 | Diagnostic.Kind.ERROR, 38 | e.toString() 39 | ) 40 | } catch (e: IOException) { 41 | processingEnv.messager.printMessage( 42 | Diagnostic.Kind.ERROR, 43 | e.toString(), 44 | typeElement 45 | ) 46 | } 47 | } 48 | 49 | fun applyTransformationType(builder: StringBuilder, transformationType: ImageTransformationType){ 50 | when(transformationType){ 51 | ImageTransformationType.CENTER_CROP -> { 52 | builder.append(".centerCrop()") 53 | } 54 | ImageTransformationType.CENTER_INSIDE -> { 55 | builder.append(".centerInside()") 56 | } 57 | ImageTransformationType.CIRCLE_CROP -> { 58 | builder.append(".circleCrop()") 59 | } 60 | ImageTransformationType.FIT_CENTER -> { 61 | builder.append(".fitCenter()") 62 | } 63 | else->{} 64 | } 65 | } 66 | 67 | fun getAppropriateSuffix(dataType: AutoGeneratorDataType): String { 68 | return when(dataType) { 69 | AutoGeneratorDataType.DATA -> Consts.converterSuffix 70 | AutoGeneratorDataType.LIST -> Consts.converterListSuffix 71 | AutoGeneratorDataType.MAP -> Consts.converterMapSuffix 72 | AutoGeneratorDataType.PAIR -> Consts.converterPairSuffix 73 | } 74 | } 75 | 76 | fun prepareJavaModelName(data: String): String { 77 | return if (data.contains("\\.")) { 78 | data.split(".").joinToString { getJavaModelName(it) } 79 | } else getJavaModelName(data) 80 | } 81 | 82 | fun isEnum(type: TypeMirror?, types: Types): Boolean { 83 | for (directSupertype in types.directSupertypes(type)) { 84 | val element = (directSupertype as DeclaredType).asElement() as TypeElement 85 | if (element.qualifiedName.contentEquals("java.lang.Enum")) { 86 | return true 87 | } 88 | if (isEnum(directSupertype, types)) { 89 | return true 90 | } 91 | } 92 | return false 93 | } 94 | 95 | fun disableIncrementalProcessing(processingEnv: ProcessingEnvironment): Boolean { 96 | return !(processingEnv.options[AUTOBINDINGS_INCREMENTAL]?.toBoolean() ?: false) 97 | } 98 | 99 | private fun getJavaModelName(item: String) = 100 | if (!item.startsWith("get")) "get${item.capitalize()}()" else item 101 | } -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/utils/ClickUtils.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.utils 2 | 3 | import com.kpstv.bindings.OnClick 4 | import com.kpstv.bindings.OnLongClick 5 | import com.squareup.javapoet.CodeBlock 6 | import com.squareup.javapoet.MethodSpec 7 | import com.squareup.javapoet.TypeName 8 | import com.squareup.javapoet.TypeSpec 9 | import javax.lang.model.element.Element 10 | import javax.lang.model.element.Modifier 11 | import javax.lang.model.element.TypeElement 12 | 13 | enum class BINDTYPE { 14 | BIND, 15 | VIEWHOLDER 16 | } 17 | 18 | object ClickUtils { 19 | private const val position = "${Consts.holder}.getAdapterPosition()" 20 | private const val contextStatement = "${Consts.holder}.itemView.getContext()" 21 | fun generateClickListener( 22 | typeElement: TypeElement, 23 | bindType: BINDTYPE, 24 | listAdapter: Boolean = false, 25 | viewType: Int 26 | ): CodeBlock { 27 | val codeBlock = CodeBlock.builder() 28 | typeElement.enclosedElements.forEach { element -> 29 | val onClickAnnotation = element.getAnnotation(OnClick::class.java) 30 | if (onClickAnnotation != null 31 | && shouldImplement(onClickAnnotation.setInViewHolder, bindType) 32 | && viewType == onClickAnnotation.viewType 33 | ) { 34 | val listener = TypeSpec.anonymousClassBuilder("") 35 | .addSuperinterface(Consts.CLASSNAME_VIEW_ONCLICK_LISTENER) 36 | .addMethod( 37 | MethodSpec.methodBuilder("onClick") 38 | .addModifiers(Modifier.PUBLIC) 39 | .returns(TypeName.VOID) 40 | .addParameter(Consts.CLASSNAME_VIEW, Consts.view) 41 | .addStatement(generateClassStatement(listAdapter, element)) 42 | .build() 43 | ) 44 | .build() 45 | 46 | val listenerBuilder = StringBuilder().apply { 47 | append("${Consts.holder}.itemView.findViewById(${onClickAnnotation.itemId}).setOnClickListener(${listener})") 48 | } 49 | codeBlock.addStatement(listenerBuilder.toString()) 50 | } 51 | 52 | val onLongClickAnnotation = element.getAnnotation(OnLongClick::class.java) 53 | if (onLongClickAnnotation != null 54 | && shouldImplement(onLongClickAnnotation.setInViewHolder, bindType) 55 | && viewType == onLongClickAnnotation.viewType 56 | ) { 57 | val listener = TypeSpec.anonymousClassBuilder("") 58 | .addSuperinterface(Consts.CLASSNAME_VIEW_ONLONGCLICK_LISTENER) 59 | .addMethod( 60 | MethodSpec.methodBuilder("onLongClick") 61 | .addModifiers(Modifier.PUBLIC) 62 | .returns(TypeName.BOOLEAN) 63 | .addParameter(Consts.CLASSNAME_VIEW, Consts.view) 64 | .addStatement(generateClassStatement(listAdapter, element)) 65 | .addStatement("return true") 66 | .build() 67 | ) 68 | .build() 69 | val listenerBuilder = StringBuilder().apply { 70 | append("${Consts.holder}.itemView.findViewById(${onLongClickAnnotation.itemId}).setOnLongClickListener(${listener})") 71 | } 72 | codeBlock.addStatement(listenerBuilder.toString()) 73 | } 74 | } 75 | 76 | return codeBlock.build() 77 | } 78 | 79 | private fun generateClassStatement(listAdapter: Boolean, element: Element) = 80 | if (listAdapter) 81 | "${Consts.className}.${element.simpleName}(${contextStatement}, getItem(${position}), ${position})" 82 | else "${Consts.className}.${element.simpleName}(${contextStatement}, ${Consts.dataSet}.get(${position}), ${position})" 83 | 84 | 85 | private fun shouldImplement(setInViewHolder: Boolean, bindType: BINDTYPE) = 86 | ((setInViewHolder && bindType == BINDTYPE.VIEWHOLDER) || (!setInViewHolder && bindType == BINDTYPE.BIND)) 87 | } -------------------------------------------------------------------------------- /sample-ktx/src/main/java/com/kpstv/sample_ktx/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.sample_ktx 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.view.View 6 | import android.widget.Toast 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import com.kpstv.bindings.* 10 | import com.kpstv.sample_ktx.Utils.createRandomImageUrl 11 | import com.kpstv.sample_ktx.Utils.createRandomTag 12 | import com.kpstv.sample_ktx.data.Data 13 | import com.kpstv.sample_ktx.data.mapToDomain 14 | import com.kpstv.sample_ktx.databinding.ItemSmallLayoutBinding 15 | import com.kpstv.samplektx.data.DataDomain 16 | import com.squareup.sqldelight.android.AndroidSqliteDriver 17 | import com.squareup.sqldelight.db.SqlDriver 18 | import kotlinx.android.synthetic.main.activity_main.* 19 | 20 | class MainActivity : AppCompatActivity() { 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContentView(R.layout.activity_main) 25 | 26 | /** 27 | * IRW, you should inject database using a dependency injection framework to avoid recreating of database. 28 | */ 29 | val driver: SqlDriver = AndroidSqliteDriver(Database.Schema, this, "data.db") 30 | val database = Database(driver, DataDomainAdapter = DataDomain.Adapter( 31 | tagsAdapter = SQLDelightAdaptersImpl.tagsConverter 32 | )) 33 | 34 | val models = listOf( 35 | Data("Random1", createRandomImageUrl(), createRandomTag()), 36 | Data("Random2", createRandomImageUrl(), createRandomTag()), 37 | Data("Random3", createRandomImageUrl(), createRandomTag()), 38 | Data("Random4", createRandomImageUrl(), createRandomTag()), 39 | Data("Random5", createRandomImageUrl(), createRandomTag()), 40 | Data("Random6", createRandomImageUrl(), createRandomTag()) 41 | ) 42 | 43 | // Deleting all queries to simulate random data generate behaviour 44 | database.dataDomainQueries.deleteAll() 45 | 46 | /** 47 | * Insert all queries into database. This is to show working of [SQLDelightAdapters.tagsConverter] (if this would've failed then app will crash) 48 | * IRW, domain model must be fetched on app launched and should be map into base model class for further use. 49 | */ 50 | models.mapToDomain().forEach { 51 | database.dataDomainQueries.insert(it) 52 | } 53 | 54 | recyclerView.layoutManager = LinearLayoutManager(this) 55 | recyclerView.adapter = BindTestAdapter(TestAdapter()).apply { submitList(models) } 56 | } 57 | } 58 | 59 | /** 60 | * An example of using RecyclerView's modern ListAdapter 61 | */ 62 | @RecyclerViewListAdapter(dataSetType = Data::class) 63 | class TestAdapter { 64 | 65 | @DiffContentSame 66 | fun diffContentSame(oldItem: Data, newItemSame: Data) = oldItem == newItemSame 67 | 68 | @DiffItemSame 69 | fun diffItemSame(oldItem: Data, newItemSame: Data) = oldItem.imageUrl == newItemSame.imageUrl 70 | 71 | @GlideLoadArray( 72 | GlideLoad(R.id.item_small_image, 73 | "imageUrl", 74 | transformationType = ImageTransformationType.CIRCLE_CROP, 75 | errorRes = R.mipmap.ic_launcher) 76 | ) 77 | @OnBind(R.layout.item_small_layout) 78 | fun bind(view: View, item: Data, position: Int) = with(ItemSmallLayoutBinding.bind(view)) { 79 | itemTitle.text = item.name 80 | itemTags.text = item.tags.joinToString(", ") 81 | } 82 | 83 | @GlideLoadArray( 84 | GlideLoad(R.id.item_big_image, 85 | "imageUrl", 86 | transformationType = ImageTransformationType.CENTER_CROP, 87 | errorRes = R.mipmap.ic_launcher) 88 | ) 89 | @OnBind(R.layout.item_big_layout, 2) 90 | fun bind2(view: View, item: Data, position: Int) { 91 | // blank af 92 | } 93 | 94 | /** Alternate layout swaps logic */ 95 | @ItemViewType 96 | fun viewType(position: Int): Int = position % 2 * 2 97 | 98 | /** Single click is enabled for small layout */ 99 | @OnClick(R.id.mainLayout, false) 100 | fun onClick(context: Context, item: Data, position: Int) = with(context) { 101 | Toast.makeText(this, "Click, Position: $position", Toast.LENGTH_SHORT).show() 102 | } 103 | 104 | /** Long click is enabled for big layout */ 105 | @OnLongClick(R.id.mainLayout, viewType = 2) 106 | fun onLongClick(context: Context, item: Data, position: Int) = with(context) { 107 | Toast.makeText(this, "LongClick, Position: $position", Toast.LENGTH_SHORT).show() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/utils/Consts.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.utils 2 | 3 | import com.squareup.javapoet.ClassName 4 | 5 | object Consts { 6 | internal const val ISOLATING_ANNOTATION_PROCESSORS_INDICATOR = 7 | "org.gradle.annotation.processing.isolating" 8 | 9 | const val AUTOBINDINGS_INCREMENTAL = "autobindings.incremental" 10 | 11 | const val adapterPrefix = "Bind" 12 | const val holderSuffix = "Holder" 13 | const val converterSuffix = "Converter" 14 | const val converterListSuffix = "ListConverter" 15 | const val converterMapSuffix = "MapConverter" 16 | const val converterPairSuffix = "PairConverter" 17 | 18 | const val adapterSuffix = "Adapter" 19 | const val adapterListSuffix = "ListAdapter" 20 | const val adapterMapSuffix = "MapAdapter" 21 | const val adapterPairSuffix = "PairAdapter" 22 | 23 | const val toConvertMethod = "toStringFrom" 24 | const val fromConvertMethod = "fromStringTo" 25 | 26 | const val view = "view" 27 | const val dataSet = "dataSet" 28 | const val className = "className" 29 | const val position = "position" 30 | const val holder = "holder" 31 | const val converterName = "model" 32 | const val decodeMethod = "decode" 33 | const val encodeMethod = "encode" 34 | 35 | const val adapter = "adapter" 36 | 37 | val GENERATED_SQLDELIGHTADAPTER = ClassName.get( 38 | "com.kpstv.autobindings", 39 | "SQLDelightAdapters" 40 | ) 41 | 42 | val CLASSNAME_LISTADAPTER = ClassName.get( 43 | "androidx.recyclerview.widget", 44 | "ListAdapter" 45 | ) 46 | val CLASSNAME_DIFFUTIL = ClassName.get( 47 | "androidx.recyclerview.widget", 48 | "DiffUtil", 49 | "ItemCallback" 50 | ) 51 | val CLASSNAME_RECYCLERVIEW = ClassName.get( 52 | "androidx.recyclerview.widget", 53 | "RecyclerView" 54 | ) 55 | val CLASSNAME_VIEWHOLDER = ClassName.get( 56 | CLASSNAME_RECYCLERVIEW.canonicalName(), 57 | "ViewHolder" 58 | ) 59 | val CLASSNAME_JAVALIST = ClassName.get( 60 | "java.util", 61 | "List" 62 | ) 63 | val CLASSNAME_VIEW: ClassName? = ClassName.get( 64 | "android.view", 65 | "View" 66 | ) 67 | val CLASSNAME_VIEWGROUP: ClassName? = ClassName.get( 68 | "android.view", 69 | "ViewGroup" 70 | ) 71 | 72 | val CLASSNAME_VIEW_ONCLICK_LISTENER = ClassName.get( 73 | "android.view", 74 | "View", 75 | "OnClickListener" 76 | ) 77 | 78 | val CLASSNAME_VIEW_ONLONGCLICK_LISTENER = ClassName.get( 79 | "android.view", 80 | "View", 81 | "OnLongClickListener" 82 | ) 83 | val CLASSNAME_IMAGEVIEW = ClassName.get( 84 | "android.widget", 85 | "ImageView" 86 | ) 87 | 88 | val CLASSNAME_TYPECONVERTER = ClassName.get( 89 | "androidx.room", 90 | "TypeConverter" 91 | ) 92 | 93 | val CLASSNAME_NULLABLE = ClassName.get( 94 | "org.jetbrains.annotations", 95 | "Nullable" 96 | ) 97 | 98 | val CLASSNAME_GSON = ClassName.get( 99 | "com.google.gson", 100 | "Gson" 101 | ) 102 | 103 | val ClASSNAME_MOSHI = ClassName.get( 104 | "com.squareup.moshi", 105 | "Moshi" 106 | ) 107 | 108 | val CLASSNAME_JSONADAPTER = ClassName.get( 109 | "com.squareup.moshi", 110 | "JsonAdapter" 111 | ) 112 | 113 | val CLASSNAME_TYPE = ClassName.get( 114 | "java.lang.reflect", 115 | "Type" 116 | ) 117 | 118 | val CLASSNAME_TYPETOKEN = ClassName.get( 119 | "com.google.gson.reflect", 120 | "TypeToken" 121 | ) 122 | 123 | val CLASSNAME_LIST = ClassName.get( 124 | "java.util", 125 | "List" 126 | ) 127 | 128 | val CLASSNAME_MAP = ClassName.get( 129 | "java.util", 130 | "Map" 131 | ) 132 | 133 | val CLASSNAME_PAIR = ClassName.get( 134 | "android.util", 135 | "Pair" 136 | ) 137 | 138 | val CLASSNAME_KSERIALIZER = ClassName.get( 139 | "kotlinx.serialization", 140 | "KSerializer" 141 | ) 142 | 143 | val CLASSNAME_SERIALIZERSKT = ClassName.get( 144 | "kotlinx.serialization", 145 | "SerializersKt" 146 | ) 147 | 148 | val CLASSANAME_KX_JSON = ClassName.get( 149 | "kotlinx.serialization.json", 150 | "Json" 151 | ) 152 | 153 | val CLASSNAME_KX_REFLECTION = ClassName.get( 154 | "kotlin.jvm.internal", 155 | "Reflection" 156 | ) 157 | 158 | val CLASSNAME_MOSHI_TYPES = ClassName.get( 159 | "com.squareup.moshi", 160 | "Types" 161 | ) 162 | 163 | val CLASSNAME_KTYPE_PROJECTION = ClassName.get( 164 | "kotlin.reflect", 165 | "KTypeProjection" 166 | ) 167 | 168 | val CLASSNAME_COLUMNADAPTER = ClassName.get( 169 | "com.squareup.sqldelight", 170 | "ColumnAdapter" 171 | ) 172 | } -------------------------------------------------------------------------------- /publish.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | apply plugin: 'signing' 3 | 4 | if (plugins.hasPlugin('com.android.library')) { 5 | task androidJavadocs(type: Javadoc) { 6 | source = android.sourceSets.main.java.srcDirs 7 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 8 | android.libraryVariants.all { variant -> 9 | if (variant.name == 'release') { 10 | owner.classpath += variant.javaCompileProvider.get().classpath 11 | } 12 | } 13 | exclude '**/R.html', '**/R.*.html', '**/index.html' 14 | } 15 | 16 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { 17 | archiveClassifier.set('javadoc') 18 | from androidJavadocs.destinationDir 19 | } 20 | 21 | task androidSourcesJar(type: Jar) { 22 | archiveClassifier.set('sources') 23 | from android.sourceSets.main.java.srcDirs 24 | } 25 | } else { 26 | java { 27 | withJavadocJar() 28 | withSourcesJar() 29 | } 30 | javadoc { 31 | if (JavaVersion.current().isJava9Compatible()) { 32 | options.addBooleanOption('html5', true) 33 | } 34 | } 35 | } 36 | 37 | ext["signing.keyId"] = '' 38 | ext["signing.password"] = '' 39 | ext["signing.secretKeyRingFile"] = '' 40 | ext["ossrhUsername"] = '' 41 | ext["ossrhPassword"] = '' 42 | 43 | File secretPropsFile = project.rootProject.file('local.properties') 44 | if (secretPropsFile.exists()) { 45 | println "Found secret props file, loading props" 46 | Properties p = new Properties() 47 | p.load(new FileInputStream(secretPropsFile)) 48 | p.each { name, value -> 49 | ext[name] = value 50 | } 51 | } else { 52 | println "No props file, loading env vars" 53 | ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') 54 | ext["signing.password"] = System.getenv('SIGNING_PASSWORD') 55 | ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE') 56 | ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') 57 | ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') 58 | } 59 | 60 | nexusStaging { 61 | packageGroup = PUBLISH_GROUP_ID 62 | stagingProfileId = 'c51d602c035a' // dummy example, replace with your own! 63 | username = ossrhUsername 64 | password = ossrhPassword 65 | } 66 | 67 | afterEvaluate { project -> 68 | def plugins = project.getPlugins() 69 | 70 | publishing { 71 | publications { 72 | release(MavenPublication) { 73 | if (plugins.hasPlugin('com.android.library')) { 74 | from components.release 75 | artifact androidSourcesJar 76 | } else { 77 | from components.java 78 | } 79 | 80 | groupId = PUBLISH_GROUP_ID 81 | artifactId = PUBLISH_ARTIFACT_ID 82 | version = PUBLISH_VERSION 83 | 84 | pom { 85 | name = PUBLISH_ARTIFACT_ID 86 | description = 'AutoBindings for Android' 87 | // If your project has a dedicated site, use its URL here 88 | url = 'https://github.com/KaustubhPatange/AutoBindings' 89 | licenses { 90 | license { 91 | name = 'The Apache License, Version 2.0' 92 | url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' 93 | } 94 | } 95 | developers { 96 | developer { 97 | id = 'kp2016' 98 | name = 'Kaustubh Patange' 99 | email = 'developerkp16@gmail.com' 100 | } 101 | } 102 | // Version control info, if you're using GitHub, follow the format as seen here 103 | scm { 104 | connection = 'scm:git:github.com/KaustubhPatange/AutoBindings.git' 105 | developerConnection = 'scm:git:ssh://github.com/KaustubhPatange/AutoBindings.git' 106 | url = 'https://github.com/KaustubhPatange/AutoBindings/tree/master' 107 | } 108 | } 109 | } 110 | } 111 | repositories { 112 | // The repository to publish to, Sonatype/MavenCentral 113 | maven { 114 | // This is an arbitrary name, you may also use "mavencentral" or 115 | // any other name that's descriptive for you 116 | name = "sonatype" 117 | 118 | def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 119 | def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/" 120 | // You only need this if you want to publish snapshots, otherwise just set the URL 121 | // to the release repo directly 122 | url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl 123 | 124 | // The username and password we've fetched earlier 125 | credentials { 126 | username ossrhUsername 127 | password ossrhPassword 128 | } 129 | } 130 | } 131 | } 132 | 133 | signing { 134 | sign publishing.publications.release 135 | } 136 | } -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/abstract/BaseAutoGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.abstract 2 | 3 | import com.kpstv.bindings.ConverterType 4 | import com.kpstv.processor.utils.AutoGeneratorType 5 | import com.kpstv.processor.utils.Consts 6 | import com.squareup.javapoet.* 7 | import java.io.IOException 8 | 9 | abstract class BaseAutoGenerator { 10 | 11 | protected abstract val converterType: AutoGeneratorType 12 | 13 | protected abstract val serializerType: ConverterType 14 | 15 | protected abstract val baseDataType: TypeName 16 | 17 | protected abstract val typeSpecBuilder: TypeSpec.Builder 18 | 19 | protected abstract fun encodeBuilder(): MethodSpec.Builder 20 | 21 | protected abstract fun decodeBuilder(): MethodSpec.Builder 22 | 23 | open fun create() { 24 | val toMethodCode = CodeBlock.builder() 25 | .addStatement("if (${Consts.converterName} == null) return null") 26 | 27 | val fromMethodCode = CodeBlock.builder() 28 | .addStatement("if (${Consts.converterName} == null) return null") 29 | 30 | when(serializerType) { 31 | ConverterType.GSON -> { 32 | val typeStatement = CodeBlock.builder() 33 | .addStatement("\$T type = new \$T<\$T>(){}.getType()", Consts.CLASSNAME_TYPE, Consts.CLASSNAME_TYPETOKEN, baseDataType) 34 | .addStatement("\$T gson = new \$T()", Consts.CLASSNAME_GSON, Consts.CLASSNAME_GSON) 35 | .build() 36 | toMethodCode 37 | .add(typeStatement) 38 | .addStatement("return gson.toJson(${Consts.converterName}, type)") 39 | 40 | fromMethodCode 41 | .add(typeStatement) 42 | .addStatement("return gson.fromJson(${Consts.converterName}, type)") 43 | } 44 | ConverterType.MOSHI -> { 45 | val typeStatement = 46 | if (baseDataType is ParameterizedTypeName) { 47 | val parameterizedTypeName = (baseDataType as ParameterizedTypeName) 48 | val size = parameterizedTypeName.typeArguments.size 49 | val builder = CodeBlock.builder().add("\$T.newParameterizedType(\$T.class,", Consts.CLASSNAME_MOSHI_TYPES, parameterizedTypeName.rawType) 50 | for ((index, type) in parameterizedTypeName.typeArguments.withIndex()) { 51 | var text = "\$T.class" 52 | if (index != size -1) 53 | text += "," 54 | builder.add(text, type) 55 | } 56 | builder.add(")").build() 57 | } else 58 | CodeBlock.builder().add("\$T.class", baseDataType).build() 59 | 60 | toMethodCode 61 | .add("return new \$T.Builder().build().adapter(", Consts.ClASSNAME_MOSHI) 62 | .add(typeStatement) 63 | .add(").toJson(${Consts.converterName});") 64 | fromMethodCode 65 | .add("try {\n") 66 | .add("\treturn (\$T) new \$T.Builder().build().adapter(", baseDataType, Consts.ClASSNAME_MOSHI) 67 | .add(typeStatement) 68 | .add(").fromJson(${Consts.converterName});\n") 69 | .add("} catch (\$T e) {\n", IOException::class.java) 70 | .addStatement("\te.printStackTrace()") 71 | .addStatement("\treturn null") 72 | .add("}\n") 73 | } 74 | ConverterType.KOTLIN_SERIALIZATION -> { 75 | val typeStatement = 76 | if (baseDataType is ParameterizedTypeName) { 77 | val parameterizedTypeName = (baseDataType as ParameterizedTypeName) 78 | val size = parameterizedTypeName.typeArguments.size 79 | val builder = CodeBlock.builder().add("\$T.typeOf(\$T.class,", Consts.CLASSNAME_KX_REFLECTION, parameterizedTypeName.rawType) 80 | for ((index, type) in parameterizedTypeName.typeArguments.withIndex()) { 81 | var text = "\$T.Companion.invariant(\$T.typeOf(\$T.class))" 82 | if (index != size -1) 83 | text += "," 84 | builder.add(text, Consts.CLASSNAME_KTYPE_PROJECTION, Consts.CLASSNAME_KX_REFLECTION, type) 85 | } 86 | builder.add("))").build() 87 | } else 88 | CodeBlock.builder().add("\$T.typeOf(\$T.class))", Consts.CLASSNAME_KX_REFLECTION, baseDataType).build() 89 | 90 | val serializer = CodeBlock.builder() 91 | .add("\$T serializer = \$T.serializer(\$T.Default.getSerializersModule(), ", 92 | Consts.CLASSNAME_KSERIALIZER, Consts.CLASSNAME_SERIALIZERSKT, Consts.CLASSANAME_KX_JSON,) 93 | .add(typeStatement) 94 | .build() 95 | 96 | toMethodCode 97 | .addStatement(serializer) 98 | .addStatement("return \$T.Default.encodeToString(serializer, ${Consts.converterName})", Consts.CLASSANAME_KX_JSON) 99 | fromMethodCode 100 | .addStatement(serializer) 101 | .addStatement("return (\$T)\$T.Default.decodeFromString(serializer, ${Consts.converterName})", baseDataType, Consts.CLASSANAME_KX_JSON) 102 | } 103 | } 104 | 105 | typeSpecBuilder.addMethod(encodeBuilder().addCode(toMethodCode.build()).build()) 106 | typeSpecBuilder.addMethod(decodeBuilder().addCode(fromMethodCode.build()).build()) 107 | } 108 | } -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/generators/TypeConverterGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.generators 2 | 3 | import com.kpstv.bindings.ConverterType 4 | import com.kpstv.processor.utils.AutoGeneratorDataType 5 | import com.kpstv.processor.utils.Consts 6 | import com.squareup.javapoet.* 7 | import java.util.* 8 | import javax.lang.model.element.Modifier 9 | 10 | object TypeConverterGenerator { 11 | fun create(typeSpecBuilder: TypeSpec.Builder, converterType: ConverterType, originalClassName: ClassName, generatorDataType: AutoGeneratorDataType) { 12 | /* val parameterizedTypeName = ParameterizedTypeName.get(Consts.CLASSNAME_LIST, originalClassName) 13 | 14 | val toMethodCode = CodeBlock.builder() 15 | .addStatement("if (${Consts.converterName} == null) return null") 16 | 17 | val fromMethodCode = CodeBlock.builder() 18 | .addStatement("if (${Consts.converterName} == null) return null") 19 | 20 | when(converterType) { 21 | ConverterType.GSON -> { 22 | val typeStatement = CodeBlock.builder() 23 | .addStatement("\$T type = new \$T<${parameterizedTypeName}>(){}.getType()", Consts.CLASSNAME_TYPE, Consts.CLASSNAME_TYPETOKEN) 24 | .addStatement("\$T gson = new \$T()", Consts.CLASSNAME_GSON, Consts.CLASSNAME_GSON) 25 | .build() 26 | toMethodCode 27 | .add(typeStatement) 28 | .addStatement("return gson.toJson(${Consts.converterName}, type)") 29 | 30 | fromMethodCode 31 | .add(typeStatement) 32 | .addStatement("return gson.fromJson(${Consts.converterName}, type)") 33 | } 34 | ConverterType.MOSHI -> { 35 | *//** 36 | * For List data classes needs @JsonClass(generateAdapter = true) annotation 37 | *//* 38 | val typeStatement = if (!isListConverter) 39 | CodeBlock.builder().add("\$T.class", originalClassName).build() 40 | else CodeBlock.builder().add("\$T.newParameterizedType(\$T.class, \$T.class)", Consts.CLASSNAME_MOSHI_TYPES, List::class.java, originalClassName).build() 41 | toMethodCode 42 | .add("return new \$T.Builder().build().adapter(", Consts.ClASSNAME_MOSHI) 43 | .add(typeStatement) 44 | .add(").toJson(${Consts.converterName});") 45 | fromMethodCode 46 | .add("try {\n") 47 | .add("\treturn (${parameterizedTypeName}) new \$T.Builder().build().adapter(", Consts.ClASSNAME_MOSHI) 48 | .add(typeStatement) 49 | .add(").fromJson(${Consts.converterName});\n") 50 | .add("} catch (\$T e) {\n", IOException::class.java) 51 | .addStatement("\te.printStackTrace()") 52 | .addStatement("\treturn null") 53 | .add("}\n") 54 | } 55 | ConverterType.KOTLIN_SERIALIZATION -> { 56 | *//** 57 | * Class requires @Serializable annotation 58 | *//* 59 | val reflectionType = if (!isListConverter) 60 | CodeBlock.builder().add("\$T.typeOf(\$T.class))", Consts.CLASSNAME_KX_REFLECTION, originalClassName).build() 61 | else 62 | CodeBlock.builder() 63 | .add("\$T.typeOf(\$T.class, \$T.Companion.invariant(\$T.typeOf(\$T.class))))", 64 | Consts.CLASSNAME_KX_REFLECTION, List::class.java, Consts.CLASSNAME_KTYPE_PROJECTION, Consts.CLASSNAME_KX_REFLECTION, originalClassName) 65 | .build() 66 | 67 | val serializer = CodeBlock.builder() 68 | .add("\$T serializer = \$T.serializer(\$T.Default.getSerializersModule(), ", 69 | Consts.CLASSNAME_KSERIALIZER, Consts.CLASSNAME_SERIALIZERSKT, Consts.CLASSANAME_KX_JSON,) 70 | .add(reflectionType) 71 | .build() 72 | 73 | toMethodCode 74 | .addStatement(serializer) 75 | .addStatement("return \$T.Default.encodeToString(serializer, ${Consts.converterName})", Consts.CLASSANAME_KX_JSON) 76 | fromMethodCode 77 | .addStatement(serializer) 78 | .addStatement("return (${parameterizedTypeName})\$T.Default.decodeFromString(serializer, ${Consts.converterName})", Consts.CLASSANAME_KX_JSON) 79 | } 80 | } 81 | 82 | val toMethodGenerator = MethodSpec.methodBuilder(Consts.toConvertMethod + originalClassName.simpleName()) 83 | .addModifiers(Modifier.PUBLIC, Modifier.STATIC) 84 | .addAnnotation(Consts.CLASSNAME_TYPECONVERTER) 85 | .returns(String::class.java) 86 | .addCode(toMethodCode.build()) 87 | 88 | val fromMethodGenerator = MethodSpec.methodBuilder(Consts.fromConvertMethod + originalClassName.simpleName()) 89 | .addModifiers(Modifier.PUBLIC, Modifier.STATIC) 90 | .addAnnotation(Consts.CLASSNAME_TYPECONVERTER) 91 | .addParameter(String::class.java, Consts.converterName) 92 | .addCode(fromMethodCode.build()) 93 | 94 | if (!isListConverter) { 95 | toMethodGenerator.addParameter(originalClassName, Consts.converterName) 96 | fromMethodGenerator.returns(originalClassName) 97 | } 98 | else { 99 | toMethodGenerator.addParameter(parameterizedTypeName, Consts.converterName) 100 | fromMethodGenerator.returns(parameterizedTypeName) 101 | } 102 | 103 | typeSpecBuilder.addMethod(toMethodGenerator.build()) 104 | typeSpecBuilder.addMethod(fromMethodGenerator.build())*/ 105 | } 106 | } -------------------------------------------------------------------------------- /lint/src/test/java/com/kpstv/lint/WrongRecyclerViewImplementationTests.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.lint 2 | 3 | import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin 4 | import com.android.tools.lint.checks.infrastructure.TestLintTask.lint 5 | import com.android.tools.lint.checks.infrastructure.TestMode 6 | import com.kpstv.lint.detectors.RecyclerViewDetector 7 | import com.kpstv.lint.utils.Stubs 8 | import org.junit.Test 9 | 10 | class WrongRecyclerViewImplementationTests { 11 | 12 | @Test 13 | fun onBindTest() { 14 | val stubFile = kotlin( 15 | """ 16 | package com.kpstv.lint 17 | import com.kpstv.bindings.RecyclerViewAdapter 18 | @RecyclerViewAdapter 19 | class TestAdapter { 20 | } 21 | """ 22 | ).indented() 23 | lint().files(Stubs.RECYCLERVIEW_ANNOTATION, Stubs.LAYOUT_RES_ANNOTATION, Stubs.ONBIND_ANNOTATION, stubFile) 24 | .issues(RecyclerViewDetector.ISSUE_ON_BIND) 25 | .run() 26 | .expect( 27 | """ 28 | src/com/kpstv/lint/TestAdapter.kt:4: Warning: There must be at least one @OnBind() method [onBindNeeded] 29 | class TestAdapter { 30 | ~~~~~~~~~~~ 31 | 0 errors, 1 warnings 32 | """.trimIndent(), 33 | testMode = TestMode.DEFAULT 34 | ) 35 | } 36 | 37 | @Test 38 | fun onRecyclerViewDataSetParameterNotEmptyTest() { 39 | val stubFile = kotlin( 40 | """ 41 | package com.kpstv.lint 42 | import com.kpstv.bindings.RecyclerViewAdapter 43 | @RecyclerViewAdapter() 44 | class TestAdapter { 45 | } 46 | """ 47 | ).indented() 48 | lint().files(Stubs.RECYCLERVIEW_ANNOTATION, stubFile) 49 | .issues(RecyclerViewDetector.ISSUE_RECYCLERVIEW) 50 | .run() 51 | .expect( 52 | """ 53 | src/com/kpstv/lint/TestAdapter.kt:4: Warning: Annotation must have a dataSetType parameter [recyclerViewParameterMissing] 54 | class TestAdapter { 55 | ~~~~~~~~~~~ 56 | 0 errors, 1 warnings 57 | """.trimIndent() 58 | ) 59 | } 60 | 61 | @Test 62 | fun onBindMethodImplementedIncorrectlyTest() { 63 | val stubFile = kotlin( 64 | """ 65 | package com.kpstv.lint 66 | 67 | import com.kpstv.bindings.RecyclerViewAdapter 68 | import com.kpstv.bindings.OnBind 69 | 70 | @RecyclerViewAdapter 71 | class TestAdapter { 72 | @OnBind(layoutId = 12) 73 | fun bind() {} 74 | } 75 | """ 76 | ).indented() 77 | lint().files( 78 | Stubs.CLASS_VIEW, 79 | Stubs.RECYCLERVIEW_ANNOTATION, 80 | Stubs.ONBIND_ANNOTATION, 81 | stubFile 82 | ) 83 | .issues(RecyclerViewDetector.ISSUE_INCORRECT_BIND) 84 | .allowCompilationErrors() 85 | .run() 86 | .expect( 87 | """ 88 | src/com/kpstv/lint/TestAdapter.kt:8: Error: @OnBind method is implemented incorrectly. [incorrectBindImplementation] 89 | @OnBind(layoutId = 12) 90 | ^ 91 | 1 errors, 0 warnings 92 | """.trimIndent() 93 | ) 94 | } 95 | 96 | @Test 97 | fun onClickMethodImplementedIncorrectlyTest() { 98 | val stubFile = kotlin( 99 | """ 100 | package com.kpstv.lint 101 | 102 | import com.kpstv.bindings.OnClick 103 | import com.kpstv.bindings.RecyclerViewAdapter 104 | 105 | @RecyclerViewAdapter 106 | class TestAdapter { 107 | @OnClick(21) 108 | fun click() { } 109 | } 110 | """ 111 | ).indented() 112 | lint().files( 113 | Stubs.RECYCLERVIEW_ANNOTATION, 114 | Stubs.ONCLICK_ANNOTATION, 115 | stubFile 116 | ) 117 | .issues(RecyclerViewDetector.ISSUE_INCORRECT_ONCLICK) 118 | .run() 119 | .expect( 120 | """ 121 | src/com/kpstv/lint/TestAdapter.kt:8: Error: @Onclick method parameters are implemented wrong. [incorrectOnClickImplementation] 122 | @OnClick(21) 123 | ^ 124 | 1 errors, 0 warnings 125 | """.trimIndent() 126 | ) 127 | } 128 | 129 | @Test 130 | fun itemViewTypeImplementedIncorrectlyTest() { 131 | val stubFile = kotlin( 132 | """ 133 | package com.kpstv.lint 134 | 135 | import com.kpstv.bindings.ItemViewType 136 | import com.kpstv.bindings.RecyclerViewAdapter 137 | 138 | @RecyclerViewAdapter 139 | class TestAdapter { 140 | @ItemViewType 141 | fun viewType() { } 142 | } 143 | """ 144 | ).indented() 145 | lint().files( 146 | Stubs.RECYCLERVIEW_ANNOTATION, 147 | Stubs.ITEMVIEWTYPE_ANNOTATION, 148 | stubFile 149 | ).issues(RecyclerViewDetector.ISSUE_INCORRECT_ITEMVIEWTYPE) 150 | .run() 151 | .expect(""" 152 | src/com/kpstv/lint/TestAdapter.kt:8: Error: @ItemViewType method is implemented incorrectly. [incorrectItemViewTypeImplementation] 153 | @ItemViewType 154 | ^ 155 | 1 errors, 0 warnings 156 | """.trimIndent()) 157 | } 158 | } -------------------------------------------------------------------------------- /lint/src/test/java/com/kpstv/lint/utils/Stubs.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.lint.utils 2 | 3 | import com.android.tools.lint.checks.infrastructure.TestFile 4 | import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin 5 | 6 | object Stubs { 7 | val RECYCLERVIEW_ANNOTATION: TestFile = kotlin( 8 | """ 9 | package com.kpstv.bindings 10 | import kotlin.reflect.KClass 11 | 12 | @Target(AnnotationTarget.CLASS) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class RecyclerViewAdapter(val dataSetType: KClass<*> = Any::class) 15 | """ 16 | ).indented() 17 | val RECYCLERVIEWLIST_ANNOTATION: TestFile = kotlin( 18 | """ 19 | package com.kpstv.bindings 20 | import kotlin.reflect.KClass 21 | 22 | @Target(AnnotationTarget.CLASS) 23 | @Retention(AnnotationRetention.SOURCE) 24 | annotation class RecyclerViewListAdapter(val dataSetType: KClass<*> = Any::class) 25 | """ 26 | ).indented() 27 | val LAYOUT_RES_ANNOTATION = kotlin( 28 | """ 29 | package androidx.annotation 30 | annotation class LayoutRes 31 | """ 32 | ) 33 | val ONBIND_ANNOTATION = kotlin( 34 | """ 35 | package com.kpstv.bindings 36 | import androidx.annotation.LayoutRes 37 | 38 | @Target(AnnotationTarget.FUNCTION) 39 | @Retention(AnnotationRetention.SOURCE) 40 | annotation class OnBind(@LayoutRes val layoutId: Int, val viewType: Int = 0) 41 | """ 42 | ).indented() 43 | val ONCLICK_ANNOTATION = kotlin( 44 | """ 45 | package com.kpstv.bindings 46 | 47 | @Target(AnnotationTarget.FUNCTION) 48 | @Retention(AnnotationRetention.SOURCE) 49 | annotation class OnClick( 50 | val itemId: Int, 51 | val setInViewHolder: Boolean = true, 52 | val viewType: Int = 0 53 | ) 54 | """ 55 | ).indented() 56 | val ONLONGCLICK_ANNOTATION = kotlin( 57 | """ 58 | package com.kpstv.bindings 59 | 60 | @Target(AnnotationTarget.FUNCTION) 61 | @Retention(AnnotationRetention.SOURCE) 62 | annotation class OnLongClick( 63 | val itemId: Int, 64 | val setInViewHolder: Boolean = true, 65 | val viewType: Int = 0 66 | ) 67 | """ 68 | ).indented() 69 | val ITEMVIEWTYPE_ANNOTATION = kotlin( 70 | """ 71 | package com.kpstv.bindings 72 | 73 | @Target(AnnotationTarget.FUNCTION) 74 | @Retention(AnnotationRetention.SOURCE) 75 | annotation class ItemViewType 76 | """ 77 | ).indented() 78 | val DIFFITEMSAME_ANNOTATION = kotlin( 79 | """ 80 | package com.kpstv.bindings 81 | 82 | @Target(AnnotationTarget.FUNCTION) 83 | @Retention(AnnotationRetention.SOURCE) 84 | annotation class DiffItemSame 85 | """ 86 | ).indented() 87 | val DIFFCONTENTSAME_ANNOTATION = kotlin( 88 | """ 89 | package com.kpstv.bindings 90 | 91 | @Target(AnnotationTarget.FUNCTION) 92 | @Retention(AnnotationRetention.SOURCE) 93 | annotation class DiffContentSame 94 | """ 95 | ).indented() 96 | 97 | val CLASS_VIEW = kotlin( 98 | """ 99 | package android.view 100 | class View { } 101 | """ 102 | ).indented() 103 | 104 | val CLASS_CONTEXT = kotlin( 105 | """ 106 | package android.content 107 | class Context { } 108 | """ 109 | ).indented() 110 | 111 | val CLASS_KOTLIN_SERIALIZABLE = kotlin( 112 | """ 113 | package kotlinx.serialization 114 | annotation class Serializable 115 | """ 116 | ) 117 | 118 | val CLASS_CONVERTER_TYPE = kotlin( 119 | """ 120 | package com.kpstv.bindings 121 | 122 | enum class ConverterType { 123 | GSON, MOSHI, KOTLIN_SERIALIZATION 124 | } 125 | """ 126 | ).indented() 127 | 128 | val CLASS_JSONCLASS = kotlin( 129 | """ 130 | package com.squareup.moshi 131 | annotation class JsonClass(generateAdapter: Boolean = false) 132 | """ 133 | ) 134 | 135 | val CLASS_COLUMNADAPTER = kotlin( 136 | """ 137 | package com.squareup.sqldelight 138 | interface ColumnAdapter 139 | """ 140 | ).indented() 141 | 142 | val CONVERTER_ANNOTATIONS = kotlin( 143 | """ 144 | package com.kpstv.bindings 145 | 146 | import com.kpstv.bindings.ConverterType 147 | 148 | @Target(AnnotationTarget.CLASS) 149 | @Retention(AnnotationRetention.SOURCE) 150 | annotation class AutoGenerateConverter(val using: ConverterType) 151 | """ 152 | ).indented() 153 | 154 | val LISTCONVERTER_ANNOTATIONS = kotlin( 155 | """ 156 | package com.kpstv.bindings 157 | 158 | import com.kpstv.bindings.ConverterType 159 | 160 | @Target(AnnotationTarget.CLASS) 161 | @Retention(AnnotationRetention.SOURCE) 162 | annotation class AutoGenerateListConverter(val using: ConverterType) 163 | """ 164 | ).indented() 165 | 166 | val COLUMNADAPTER_ANNOTATIONS = kotlin( 167 | """ 168 | package com.kpstv.bindings 169 | 170 | import com.kpstv.bindings.ConverterType 171 | 172 | @Target(AnnotationTarget.CLASS) 173 | annotation class AutoGenerateSQLDelightAdapters(val using: ConverterType) 174 | """ 175 | ).indented() 176 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /sample-ktx/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/generators/BindViewGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor.generators 2 | 3 | import com.kpstv.bindings.GlideLoadArray 4 | import com.kpstv.bindings.OnBind 5 | import com.kpstv.processor.utils.BINDTYPE 6 | import com.kpstv.processor.utils.ClickUtils 7 | import com.kpstv.processor.utils.Consts 8 | import com.kpstv.processor.utils.Utils 9 | import com.squareup.javapoet.* 10 | import javax.lang.model.element.Element 11 | import javax.lang.model.element.Modifier 12 | import javax.lang.model.element.TypeElement 13 | 14 | object BindViewGenerator { 15 | 16 | fun generateOnBindViewHolder( 17 | typeSpecBuilder: TypeSpec.Builder, 18 | typeElement: TypeElement, 19 | viewHolderClassName: ClassName 20 | ) { 21 | generateOnBindViewHolder(typeSpecBuilder, typeElement, viewHolderClassName, false) 22 | } 23 | 24 | fun generateOnBindViewListHolder( 25 | typeSpecBuilder: TypeSpec.Builder, 26 | typeElement: TypeElement, 27 | viewHolderClassName: ClassName 28 | ) { 29 | generateOnBindViewHolder(typeSpecBuilder, typeElement, viewHolderClassName, true) 30 | } 31 | 32 | private fun generateOnBindViewHolder( 33 | typeSpecBuilder: TypeSpec.Builder, 34 | typeElement: TypeElement, 35 | viewHolderClassName: ClassName, 36 | isListAdapter: Boolean 37 | ) { 38 | val holderCodeGenerator = CodeBlock.builder() 39 | val bindCodeBuilder = CodeBlock.builder() 40 | 41 | typeElement.enclosedElements?.forEach { bindElement -> 42 | val bindAnnotation = bindElement.getAnnotation(OnBind::class.java) 43 | if (bindAnnotation != null) { 44 | val holderBuilder = StringBuilder().apply { 45 | append("if (viewType == ${bindAnnotation.viewType}) {\n") 46 | append("\tfinal ${viewHolderClassName.simpleName()} holder = new ${viewHolderClassName.simpleName()}(android.view.LayoutInflater.from(parent.getContext()).inflate(${bindAnnotation.layoutId}, parent, false));\n") 47 | append( 48 | ClickUtils.generateClickListener( 49 | typeElement, 50 | BINDTYPE.VIEWHOLDER, 51 | isListAdapter, 52 | bindAnnotation.viewType 53 | ) 54 | ) 55 | append("\treturn holder;\n") 56 | append("}\n") 57 | } 58 | holderCodeGenerator.add(holderBuilder.toString()) 59 | 60 | val bindBuilder = StringBuilder().apply { 61 | append("if (getItemViewType(${Consts.position}) == ${bindAnnotation.viewType}) {\n") 62 | if (isListAdapter) 63 | append("\t${Consts.className}.${bindElement?.simpleName}(${Consts.holder}.itemView, getItem(${Consts.position}), ${Consts.position});\n") 64 | else append("\t${Consts.className}.${bindElement?.simpleName}(${Consts.holder}.itemView, ${Consts.dataSet}.get(${Consts.position}), ${Consts.position});\n") 65 | append("\t${generateBindAnnotations(bindElement, isListAdapter)}") 66 | append( 67 | ClickUtils.generateClickListener( 68 | typeElement, 69 | BINDTYPE.BIND, 70 | isListAdapter, 71 | bindAnnotation.viewType 72 | ) 73 | ) 74 | append("}\n") 75 | } 76 | 77 | bindCodeBuilder.add(bindBuilder.toString()) 78 | } 79 | } 80 | 81 | typeSpecBuilder.addMethod( 82 | ViewHolderGenerator.generateOnCreateViewHolder( 83 | viewHolderClassName, 84 | holderCodeGenerator.build() 85 | ) 86 | ) 87 | typeSpecBuilder.addMethod( 88 | generateOnBindHolder( 89 | viewHolderClassName, 90 | bindCodeBuilder.build() 91 | ) 92 | ) 93 | } 94 | 95 | private fun generateOnBindHolder( 96 | viewHolderClassName: ClassName, 97 | bindCodeBlock: CodeBlock 98 | ): MethodSpec { 99 | return MethodSpec.methodBuilder("onBindViewHolder") 100 | .addModifiers(Modifier.PUBLIC) 101 | .addAnnotation(Override::class.java) 102 | .addParameter(viewHolderClassName, Consts.holder, Modifier.FINAL) 103 | .addParameter(TypeName.INT, Consts.position, Modifier.FINAL) 104 | .addCode(bindCodeBlock) 105 | /*.addCode( 106 | ClickUtils.generateClickListener( 107 | typeElement, 108 | BINDTYPE.BIND, 109 | isListAdapter, 110 | viewType 111 | ) 112 | )*/ 113 | .build() 114 | } 115 | 116 | private fun generateBindAnnotations( 117 | bindElement: Element?, 118 | isListAdapter: Boolean = false 119 | ): CodeBlock { 120 | val codeBlockBuilder = CodeBlock.builder() 121 | val getItemStatement = if (!isListAdapter) "${Consts.dataSet}.get(${Consts.position})" 122 | else "getItem(${Consts.position})" 123 | 124 | val glideLoadArrayAnnotation = bindElement?.getAnnotation(GlideLoadArray::class.java) 125 | glideLoadArrayAnnotation?.glideLoad?.forEach { 126 | val glideBlock = StringBuilder().apply { 127 | append("com.bumptech.glide.Glide.with(${Consts.holder}.itemView.getContext())") 128 | append(".load(${getItemStatement}.${Utils.prepareJavaModelName(it.data)})") 129 | append(".diskCacheStrategy(com.bumptech.glide.load.engine.DiskCacheStrategy.${it.cachingStrategyImage.name})") 130 | Utils.applyTransformationType(this, it.transformationType) 131 | if (it.errorRes != -1) { 132 | append(".error(${it.errorRes})") 133 | } 134 | if (it.placeHolderRes != -1) { 135 | append(".placeholder(${it.placeHolderRes})") 136 | } 137 | append(".into((${Consts.CLASSNAME_IMAGEVIEW.canonicalName()}) ${Consts.holder}.itemView.findViewById(${it.itemId}))") 138 | } 139 | codeBlockBuilder.apply { 140 | addStatement(glideBlock.toString()) 141 | } 142 | } 143 | return codeBlockBuilder.build() 144 | } 145 | } -------------------------------------------------------------------------------- /lint/src/main/java/com/kpstv/lint/detectors/TypeConvertDetector.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.lint.detectors 2 | 3 | import com.android.tools.lint.client.api.UElementHandler 4 | import com.android.tools.lint.detector.api.* 5 | import com.intellij.psi.PsiAnnotation 6 | import com.kpstv.bindings.AutoGenerateConverter 7 | import com.kpstv.bindings.AutoGenerateListConverter 8 | import com.kpstv.bindings.AutoGenerateSQLDelightAdapters 9 | import com.kpstv.lint.Utils 10 | import org.jetbrains.uast.UClass 11 | import org.jetbrains.uast.UMethod 12 | import org.jetbrains.uast.toUElement 13 | import org.jetbrains.uast.visitor.AbstractUastVisitor 14 | 15 | class TypeConvertDetector : Detector(), Detector.UastScanner { 16 | 17 | override fun getApplicableUastTypes() = listOf(UClass::class.java) 18 | 19 | override fun createUastHandler(context: JavaContext) = 20 | object : UElementHandler() { 21 | override fun visitClass(node: UClass) { 22 | node.accept( 23 | VisitorPattern( 24 | context 25 | ) 26 | ) 27 | } 28 | } 29 | 30 | class VisitorPattern(private val context: JavaContext) : AbstractUastVisitor() { 31 | override fun visitClass(node: UClass): Boolean { 32 | if (node.hasAnnotation(ANNOTATION_CONVERTER)) { 33 | val converterAnnotation = node.getAnnotation(ANNOTATION_CONVERTER) 34 | commonSerializationReport(node, converterAnnotation) 35 | } 36 | if (node.hasAnnotation(ANNOTATION_LIST_CONVERTER)) { 37 | val converterAnnotation = node.getAnnotation(ANNOTATION_LIST_CONVERTER) 38 | if (converterAnnotation?.findAttributeValue("using")?.text == "ConverterType.MOSHI") { 39 | if (!node.hasAnnotation(ANNOTATION_JSONCLASS)) 40 | reportNoJsonClass(node, converterAnnotation) 41 | else { 42 | val jsonClassAnnotation = node.getAnnotation(ANNOTATION_JSONCLASS) 43 | if (jsonClassAnnotation?.findAttributeValue("generateAdapter")?.text != "true") 44 | reportNoJsonClass(node, converterAnnotation) 45 | } 46 | } 47 | commonSerializationReport(node, converterAnnotation) 48 | } 49 | if (node.hasAnnotation(ANNOTATION_COLUMN_ADAPTER)) { 50 | if (!node.isInterface) 51 | context.report( 52 | issue = ISSUE_NO_INTERFACE, 53 | scopeClass = node, 54 | location = context.getLocation(node.navigationElement), 55 | message = MSG_NO_INTERFACE 56 | ) 57 | } 58 | return super.visitClass(node) 59 | } 60 | 61 | override fun visitMethod(node: UMethod): Boolean { 62 | val parentNode = node.parent.toUElement() 63 | if (parentNode is UClass && parentNode.hasAnnotation(ANNOTATION_COLUMN_ADAPTER)) { 64 | val split = INVALID_RETURN_TYPE_MATCHER.toRegex() 65 | .matchEntire(node.returnType?.canonicalText ?: "") 66 | ?.groups?.get(2) 67 | ?.value?.split(",") 68 | if (split?.last()?.trim() != CLASS_STRING) 69 | context.report( 70 | issue = ISSUE_WRONG_RETURN_TYPE, 71 | location = context.getLocation(node), 72 | message = MSG_INVALID_RETURN_TYPE 73 | ) 74 | } 75 | return super.visitMethod(node) 76 | } 77 | 78 | private fun reportNoJsonClass(node: UClass, converterAnnotation: PsiAnnotation?) { 79 | val className = Utils.getSimpleName(converterAnnotation?.qualifiedName) 80 | context.report( 81 | issue = ISSUE_NO_JSONCLASS, 82 | scopeClass = node, 83 | location = context.getLocation(node.navigationElement), 84 | message = "Class must be annotated with @JsonClass(generateAdapter = true).", 85 | quickfixData = LintFix.create() 86 | .replace() 87 | .name("Add @JsonClass(generateAdapter = true) annotation") 88 | .beginning() 89 | .with("@${ANNOTATION_JSONCLASS}(generateAdapter = true)") 90 | .reformat(true) 91 | .range(context.getLocation(node.navigationElement)) 92 | .shortenNames() 93 | .build() 94 | ) 95 | } 96 | 97 | private fun commonSerializationReport(node: UClass, converterAnnotation: PsiAnnotation?) { 98 | if (converterAnnotation?.findAttributeValue("using")?.text == "ConverterType.KOTLIN_SERIALIZATION") { 99 | if (!node.hasAnnotation(ANNOTATION_SERIALIZABLE)) { 100 | val className = Utils.getSimpleName(converterAnnotation.qualifiedName) 101 | context.report( 102 | issue = ISSUE_NO_SERIALIZABLE, 103 | scopeClass = node, 104 | location = context.getLocation(node.navigationElement), 105 | message = "Class must be annotated with @Serializable.", 106 | quickfixData = LintFix.create() 107 | .replace() 108 | .name("Add @Serializable annotation") 109 | .beginning() 110 | .with("@${ANNOTATION_SERIALIZABLE}") 111 | .reformat(true) 112 | .range(context.getLocation(node.navigationElement)) 113 | .shortenNames() 114 | .build() 115 | ) 116 | } 117 | } 118 | } 119 | } 120 | 121 | companion object { 122 | private const val ANNOTATION_CONVERTER = "com.kpstv.bindings.AutoGenerateConverter" 123 | private const val ANNOTATION_LIST_CONVERTER = "com.kpstv.bindings.AutoGenerateListConverter" 124 | private const val ANNOTATION_COLUMN_ADAPTER = "com.kpstv.bindings.AutoGenerateSQLDelightAdapters" 125 | const val ANNOTATION_SERIALIZABLE = "kotlinx.serialization.Serializable" 126 | const val ANNOTATION_JSONCLASS = "com.squareup.moshi.JsonClass" 127 | 128 | const val CLASS_STRING = "java.lang.String" 129 | const val INVALID_RETURN_TYPE_MATCHER = 130 | "com\\.squareup\\.sqldelight\\.ColumnAdapter(\\s+)?<(.*)>" 131 | 132 | private const val MSG_NO_INTERFACE = "Class must be an interface" 133 | private const val MSG_INVALID_RETURN_TYPE = 134 | "Return type of function must be of type ColumnAdapter<*, String>" 135 | 136 | private val IMPLEMENTATION = Implementation( 137 | TypeConvertDetector::class.java, 138 | Scope.JAVA_FILE_SCOPE 139 | ) 140 | 141 | val ISSUE_NO_SERIALIZABLE: Issue = Issue 142 | .create( 143 | id = "kotlinSerialization", 144 | briefDescription = "Class must be annotated with @Serializable.", 145 | explanation = """ 146 | Kotlin serialization needs classes to be annotated with @Serializable 147 | """.trimIndent(), 148 | category = Category.CORRECTNESS, 149 | priority = 9, 150 | severity = Severity.WARNING, 151 | androidSpecific = true, 152 | implementation = IMPLEMENTATION 153 | ) 154 | 155 | val ISSUE_NO_JSONCLASS: Issue = Issue 156 | .create( 157 | id = "noJsonClass", 158 | briefDescription = "Class must be annotated with @JsonClass(generateAdapter = true)", 159 | explanation = """ 160 | This is needed otherwise the program will create runtime exceptions. 161 | """.trimIndent(), 162 | category = Category.CORRECTNESS, 163 | priority = 9, 164 | severity = Severity.WARNING, 165 | androidSpecific = true, 166 | implementation = IMPLEMENTATION 167 | ) 168 | val ISSUE_NO_INTERFACE: Issue = Issue 169 | .create( 170 | id = "delightNoInterface", 171 | briefDescription = MSG_NO_INTERFACE, 172 | explanation = """ 173 | This is needed otherwise the program will create runtime exceptions. 174 | """.trimIndent(), 175 | category = Category.CORRECTNESS, 176 | priority = 10, 177 | severity = Severity.ERROR, 178 | androidSpecific = true, 179 | implementation = IMPLEMENTATION 180 | ) 181 | val ISSUE_WRONG_RETURN_TYPE: Issue = Issue 182 | .create( 183 | id = "delightWrongReturnType", 184 | briefDescription = MSG_INVALID_RETURN_TYPE, 185 | explanation = """ 186 | This is needed otherwise the program will create runtime exceptions. 187 | """.trimIndent(), 188 | category = Category.CORRECTNESS, 189 | priority = 10, 190 | severity = Severity.ERROR, 191 | androidSpecific = true, 192 | implementation = IMPLEMENTATION 193 | ) 194 | } 195 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /compiler/src/main/java/com/kpstv/processor/BindingProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.processor 2 | 3 | import com.kpstv.bindings.* 4 | import com.kpstv.processor.generators.* 5 | import com.kpstv.processor.generators.auto.SQLDelightAdapterProcessor 6 | import com.kpstv.processor.generators.auto.TypeConverterProcessor 7 | import com.kpstv.processor.utils.* 8 | import com.squareup.javapoet.* 9 | import javax.annotation.processing.AbstractProcessor 10 | import javax.annotation.processing.RoundEnvironment 11 | import javax.lang.model.SourceVersion 12 | import javax.lang.model.element.* 13 | import javax.lang.model.util.ElementFilter 14 | import javax.tools.Diagnostic 15 | 16 | class BindingProcessor : AbstractProcessor() { 17 | 18 | override fun getSupportedAnnotationTypes(): MutableSet { 19 | return mutableSetOf( 20 | AutoGenerateSQLDelightAdapters::class.java.canonicalName, 21 | AutoGenerateConverter::class.java.canonicalName, 22 | AutoGenerateMapConverter::class.java.canonicalName, 23 | AutoGeneratePairConverter::class.java.canonicalName, 24 | AutoGenerateListConverter::class.java.canonicalName, 25 | RecyclerViewListAdapter::class.java.canonicalName, 26 | RecyclerViewAdapter::class.java.canonicalName, 27 | ) 28 | } 29 | 30 | override fun getSupportedOptions(): MutableSet { 31 | if (!Utils.disableIncrementalProcessing(processingEnv)) 32 | return mutableSetOf(Consts.ISOLATING_ANNOTATION_PROCESSORS_INDICATOR) 33 | return super.getSupportedOptions() 34 | } 35 | 36 | override fun getSupportedSourceVersion(): SourceVersion { 37 | return SourceVersion.latestSupported() 38 | } 39 | 40 | override fun process( 41 | annotations: MutableSet?, 42 | env: RoundEnvironment?, 43 | ): Boolean { 44 | parseRecyclerViewAnnotation(env) 45 | parseRecyclerViewListAnnotation(env) 46 | parseAutoGenerateAnnotations(env) 47 | parseAutoGenerateSQLDelightAnnotations(env) 48 | return true 49 | } 50 | 51 | private fun parseAutoGenerateSQLDelightAnnotations(env: RoundEnvironment?) { 52 | val autoGenerateSQLDelightAnnotationTypes = 53 | env?.getElementsAnnotatedWith(AutoGenerateSQLDelightAdapters::class.java) 54 | val elements = ElementFilter.typesIn(autoGenerateSQLDelightAnnotationTypes).toMutableList() 55 | elements.forEach { typeElement -> 56 | 57 | if (!typeElement.modifiers.contains(Modifier.ABSTRACT)) 58 | processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, 59 | "${typeElement.qualifiedName}: Class must be an interface") 60 | 61 | val packageName = processingEnv.elementUtils.getPackageOf(typeElement).qualifiedName.toString() 62 | val originalClassName = ClassName.get(typeElement) 63 | val generatedClassName = ClassName.get(packageName, 64 | typeElement.simpleName.toString() + "Impl") 65 | 66 | val adapterBuilder = TypeSpec.classBuilder(generatedClassName) 67 | .addModifiers(Modifier.PUBLIC, Modifier.FINAL) 68 | .addSuperinterface(originalClassName) 69 | .addField( 70 | FieldSpec.builder(generatedClassName, 71 | "Instance", 72 | Modifier.PRIVATE, 73 | Modifier.STATIC) 74 | .initializer("new \$T()", generatedClassName).build() 75 | ) 76 | 77 | val serializerType = typeElement.getAnnotation(AutoGenerateSQLDelightAdapters::class.java).using 78 | 79 | typeElement.enclosedElements.forEach enclosed@{ enclosedElement -> 80 | val executableElement = enclosedElement as ExecutableElement 81 | val returnType = ParameterizedTypeName.get(executableElement.returnType) as ParameterizedTypeName 82 | 83 | val baseType = returnType.typeArguments[0] 84 | val toType = returnType.typeArguments[1] 85 | if (toType.simpleName() != "String") { 86 | processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, 87 | "${executableElement.simpleName}: Method's return type must be ColumnAdapter<*, String>") 88 | return@enclosed 89 | } 90 | 91 | adapterBuilder.addMethod( 92 | SQLDelightAdapterProcessor( 93 | adapterName = enclosedElement.simpleName.toString(), 94 | serializerType = serializerType, 95 | baseDataType = baseType 96 | ).generateMethod() 97 | ) 98 | adapterBuilder.addField( 99 | FieldSpec.builder(returnType, 100 | enclosedElement.simpleName.toString(), 101 | Modifier.PUBLIC, 102 | Modifier.STATIC) 103 | .initializer("Instance.${enclosedElement.simpleName}()") 104 | .build() 105 | ) 106 | } 107 | 108 | Utils.write(packageName, adapterBuilder.build(), processingEnv) 109 | } 110 | } 111 | 112 | private fun parseAutoGenerateAnnotations(env: RoundEnvironment?) { 113 | val autoGenerateAnnotationTypes = 114 | env?.getElementsAnnotatedWith(AutoGenerateConverter::class.java) 115 | val normalTypes = ElementFilter.typesIn(autoGenerateAnnotationTypes).toMutableList() 116 | 117 | val autoGenerateListAnnotationTypes = 118 | env?.getElementsAnnotatedWith(AutoGenerateListConverter::class.java) 119 | val listTypes = ElementFilter.typesIn(autoGenerateListAnnotationTypes).toMutableList() 120 | 121 | val autoGenerateMapAnnotationTypes = 122 | env?.getElementsAnnotatedWith(AutoGenerateMapConverter::class.java) 123 | val mapTypes = ElementFilter.typesIn(autoGenerateMapAnnotationTypes).toMutableList() 124 | 125 | val autoGeneratePairAnnotationTypes = 126 | env?.getElementsAnnotatedWith(AutoGeneratePairConverter::class.java) 127 | val pairTypes = ElementFilter.typesIn(autoGeneratePairAnnotationTypes).toMutableList() 128 | 129 | normalTypes.forEach { typeElement -> 130 | val annotationAttributes = typeElement.getAnnotation(AutoGenerateConverter::class.java) 131 | commonParseConverterAnnotation(typeElement, 132 | annotationAttributes.using, 133 | AutoGeneratorDataType.DATA) 134 | } 135 | listTypes.forEach { typeElement -> 136 | val annotationAttributes = 137 | typeElement.getAnnotation(AutoGenerateListConverter::class.java) 138 | commonParseConverterAnnotation(typeElement, 139 | annotationAttributes.using, 140 | AutoGeneratorDataType.LIST) 141 | } 142 | mapTypes.forEach { typeElement -> 143 | val annotationAttributes = 144 | typeElement.getAnnotation(AutoGenerateMapConverter::class.java) 145 | val mapValue = 146 | typeElement.getAnnotationClassValue { annotationAttributes.keyClass } 147 | commonParseConverterAnnotation(typeElement, 148 | annotationAttributes.using, 149 | AutoGeneratorDataType.MAP, 150 | TypeName.get(mapValue)) 151 | } 152 | pairTypes.forEach { typeElement -> 153 | val annotationAttributes = 154 | typeElement.getAnnotation(AutoGeneratePairConverter::class.java) 155 | val mapValue = 156 | typeElement.getAnnotationClassValue { annotationAttributes.keyClass } 157 | commonParseConverterAnnotation(typeElement, 158 | annotationAttributes.using, 159 | AutoGeneratorDataType.PAIR, 160 | TypeName.get(mapValue)) 161 | } 162 | } 163 | 164 | private fun commonParseConverterAnnotation( 165 | typeElement: TypeElement, 166 | using: ConverterType, 167 | generatorDataType: AutoGeneratorDataType, 168 | secondClass: TypeName? = null, 169 | ) { 170 | val packageName = 171 | processingEnv.elementUtils.getPackageOf(typeElement).qualifiedName.toString() 172 | 173 | val enumList= typeElement.enclosedElements 174 | .filter { ele -> ele.kind == ElementKind.FIELD && Utils.isEnum(ele.asType(), processingEnv.typeUtils) } 175 | .filter { ele -> ele.getAnnotation(IgnoreConverter::class.java) == null } 176 | .map { ClassName.get(it.asType()) } 177 | 178 | val typeName = typeElement.simpleName.toString() 179 | val originalClassName = ClassName.get(typeElement) 180 | 181 | val generatedClassName = 182 | ClassName.get(packageName, typeName + Utils.getAppropriateSuffix(generatorDataType)) 183 | 184 | val converterBuilder = TypeSpec.classBuilder(generatedClassName) 185 | .addModifiers(Modifier.PUBLIC) 186 | .also { 187 | TypeConverterProcessor( 188 | typeSpecBuilder = it, 189 | enumList = enumList, 190 | serializerType = using, 191 | generatorDataType = generatorDataType, 192 | firstClassType = originalClassName, 193 | secondClassType = secondClass 194 | ).create() 195 | } 196 | 197 | Utils.write(packageName, converterBuilder.build(), processingEnv, typeElement) 198 | } 199 | 200 | private fun parseRecyclerViewListAnnotation(env: RoundEnvironment?) { 201 | val recyclerViewListAnnotations = 202 | env?.getElementsAnnotatedWith(RecyclerViewListAdapter::class.java) 203 | val types = ElementFilter.typesIn(recyclerViewListAnnotations).toMutableList() 204 | 205 | types.forEach { typeElement -> 206 | val dataTypeMirror = 207 | typeElement.getAnnotationClassValue { dataSetType } 208 | 209 | val packageName = 210 | processingEnv.elementUtils.getPackageOf(typeElement).qualifiedName.toString() 211 | val typeName = typeElement.simpleName.toString() 212 | 213 | val originalClassName = ClassName.get(packageName, typeName) 214 | val generatedClassName = ClassName.get(packageName, Consts.adapterPrefix + typeName) 215 | 216 | val viewHolderClassName = 217 | ClassName.get(generatedClassName.canonicalName(), typeName + Consts.holderSuffix) 218 | 219 | val extendedAdapterClassName = ParameterizedTypeName.get( 220 | Consts.CLASSNAME_LISTADAPTER, 221 | TypeName.get(dataTypeMirror), 222 | TypeVariableName.get(viewHolderClassName.canonicalName()).box() 223 | ) 224 | 225 | val diffUtil = DiffUtilGenerator.generateDiffUtil( 226 | typeElement, 227 | TypeName.get(dataTypeMirror) 228 | ) 229 | 230 | val adapterBuilder = TypeSpec.classBuilder(generatedClassName) 231 | .addModifiers(Modifier.PUBLIC) 232 | .addField(originalClassName, Consts.className, Modifier.PRIVATE) 233 | .addType(ViewHolderGenerator.createViewHolder(viewHolderClassName)) 234 | .superclass(extendedAdapterClassName) 235 | .addMethod( 236 | MethodSpec.constructorBuilder() 237 | .addModifiers(Modifier.PUBLIC) 238 | .addParameter(originalClassName, Consts.className, Modifier.FINAL) 239 | .addStatement("super(${diffUtil})") 240 | .addStatement("this.${Consts.className} = ${Consts.className}") 241 | .build() 242 | ) 243 | .also { 244 | BindViewGenerator.generateOnBindViewListHolder(it, 245 | typeElement, 246 | viewHolderClassName) 247 | } 248 | .addMethod(ViewTypeGenerator.generateItemViewType(typeElement)) 249 | 250 | Utils.write(packageName, adapterBuilder.build(), processingEnv, typeElement) 251 | } 252 | } 253 | 254 | private fun parseRecyclerViewAnnotation(env: RoundEnvironment?) { 255 | val recyclerViewAnnotations = env?.getElementsAnnotatedWith(RecyclerViewAdapter::class.java) 256 | val types = ElementFilter.typesIn(recyclerViewAnnotations).toMutableList() 257 | types.forEach { typeElement -> 258 | val dataTypeMirror = 259 | typeElement.getAnnotationClassValue { dataSetType } 260 | 261 | val packageName = 262 | processingEnv.elementUtils.getPackageOf(typeElement).qualifiedName.toString() 263 | val typeName = typeElement.simpleName.toString() 264 | 265 | val originalClassName = ClassName.get(packageName, typeName) 266 | val generatedClassName = ClassName.get(packageName, Consts.adapterPrefix + typeName) 267 | 268 | val viewHolderClassName = 269 | ClassName.get(generatedClassName.canonicalName(), typeName + Consts.holderSuffix) 270 | 271 | // Start building adapter 272 | val extendedAdapterClassName = ClassName.get( 273 | Consts.CLASSNAME_RECYCLERVIEW.packageName(), 274 | Consts.CLASSNAME_RECYCLERVIEW.simpleName(), 275 | "Adapter<${generatedClassName}.${viewHolderClassName.simpleName()}>" 276 | ) 277 | 278 | val dataSetParameter = ParameterizedTypeName.get( 279 | ClassName.get(MutableList::class.java), 280 | TypeName.get(dataTypeMirror) 281 | ) 282 | val adapterBuilder = TypeSpec.classBuilder(generatedClassName) 283 | .addModifiers(Modifier.PUBLIC) 284 | .addType(ViewHolderGenerator.createViewHolder(viewHolderClassName)) 285 | .superclass(extendedAdapterClassName) 286 | .addField( 287 | dataSetParameter, Consts.dataSet, Modifier.PRIVATE 288 | ) 289 | .addField(originalClassName, Consts.className, Modifier.PRIVATE) 290 | .addMethod( 291 | MethodSpec.constructorBuilder() 292 | .addParameter(originalClassName, Consts.className) 293 | .addParameter(dataSetParameter, Consts.dataSet) 294 | .addStatement("this.${Consts.className} = ${Consts.className}") 295 | .addStatement("this.${Consts.dataSet} = ${Consts.dataSet}") 296 | .addModifiers(Modifier.PUBLIC) 297 | .build() 298 | ) 299 | .also { 300 | BindViewGenerator.generateOnBindViewHolder(it, 301 | typeElement, 302 | viewHolderClassName) 303 | } 304 | .addMethod(ViewTypeGenerator.generateItemViewType(typeElement)) 305 | .addMethod(ItemCountGenerator.generateGetItemCountMethod()) 306 | 307 | Utils.write(packageName, adapterBuilder.build(), processingEnv, typeElement) 308 | } 309 | } 310 | } -------------------------------------------------------------------------------- /lint/src/main/java/com/kpstv/lint/detectors/RecyclerViewDetector.kt: -------------------------------------------------------------------------------- 1 | package com.kpstv.lint.detectors 2 | 3 | import com.android.tools.lint.client.api.UElementHandler 4 | import com.android.tools.lint.detector.api.* 5 | import com.intellij.psi.PsiAnnotation 6 | import com.intellij.psi.PsiClass 7 | import com.kpstv.lint.Language 8 | import com.kpstv.lint.Utils 9 | import com.kpstv.lint.language 10 | import org.jetbrains.uast.UClass 11 | import org.jetbrains.uast.UMethod 12 | import org.jetbrains.uast.visitor.AbstractUastVisitor 13 | 14 | class RecyclerViewDetector : Detector(), Detector.UastScanner { 15 | 16 | override fun getApplicableUastTypes() = listOf(UClass::class.java) 17 | 18 | override fun createUastHandler(context: JavaContext) = 19 | object : UElementHandler() { 20 | override fun visitClass(node: UClass) { 21 | node.accept( 22 | VisitorPattern( 23 | context 24 | ) 25 | ) 26 | } 27 | } 28 | 29 | class VisitorPattern(private val context: JavaContext) : AbstractUastVisitor() { 30 | override fun visitClass(node: UClass): Boolean { 31 | commonClassDetector(context, node) 32 | return super.visitClass(node) 33 | } 34 | 35 | override fun visitMethod(node: UMethod): Boolean { 36 | if (node.hasAnnotation(ANNOTATION_ONBIND)) { 37 | if (node.parameterList.isEmpty) reportInCorrectBindAnnotation(node) 38 | else { 39 | val parameters = 40 | node.uastParameters.joinToString { it.type.canonicalText } 41 | if (!parameters.matches(DETECTOR_INCORRECT_BIND.toRegex())) 42 | reportInCorrectBindAnnotation(node) 43 | } 44 | } else if (node.hasAnnotation(ANNOTATION_ONCLICK)) { 45 | if (node.parameterList.isEmpty) reportInCorrectOnClickAnnotation(node) 46 | else { 47 | val parameters = 48 | node.uastParameters.joinToString { it.type.canonicalText } 49 | if (!parameters.matches(DETECTOR_INCORRECT_CLICK.toRegex())) 50 | reportInCorrectOnClickAnnotation(node) 51 | } 52 | } else if (node.hasAnnotation(ANNOTATION_ONLONGCLICK)) { 53 | if (node.parameterList.isEmpty) reportInCorrectOnLongClickAnnotation(node) 54 | else { 55 | val parameters = 56 | node.uastParameters.joinToString { it.type.canonicalText } 57 | if (!parameters.matches(DETECTOR_INCORRECT_CLICK.toRegex())) 58 | reportInCorrectOnLongClickAnnotation(node) 59 | } 60 | } else if (node.hasAnnotation(ANNOTATION_ITEMVIEWTYPE)) { 61 | if (node.parameterList.isEmpty) reportInCorrectViewTypeAnnotation(node) 62 | else if (node.returnType?.canonicalText != "int") 63 | reportInCorrectViewTypeAnnotation(node) 64 | else { 65 | val parameters = 66 | node.uastParameters.joinToString { it.type.canonicalText } 67 | if (parameters != DETECTOR_INCORRECT_ITEMVIEWTYPE) 68 | reportInCorrectViewTypeAnnotation(node) 69 | } 70 | } else if (node.hasAnnotation(ANNOTATION_DIFFITEMSAME)) { 71 | if (node.parameterList.isEmpty) reportInCorrectDiffItemSameAnnotation(node) 72 | else if (node.returnType?.canonicalText != "boolean") 73 | reportInCorrectDiffItemSameAnnotation(node) 74 | else { 75 | if (node.uastParameters.size != 2) 76 | reportInCorrectDiffItemSameAnnotation(node) 77 | } 78 | } else if (node.hasAnnotation(ANNOTATION_DIFFCONTENTSAME)) { 79 | if (node.parameterList.isEmpty) reportInCorrectDiffContentSameAnnotation(node) 80 | else if (node.returnType?.canonicalText != "boolean") 81 | reportInCorrectDiffContentSameAnnotation(node) 82 | else { 83 | if (node.uastParameters.size != 2) 84 | reportInCorrectDiffItemSameAnnotation(node) 85 | } 86 | } 87 | return super.visitMethod(node) 88 | } 89 | 90 | private fun commonClassDetector(context: JavaContext, clazz: UClass) { 91 | if (clazz.hasAnnotation(ANNOTATION_RECYCLERVIEW)) { 92 | val annotation = clazz.getAnnotation(ANNOTATION_RECYCLERVIEW) 93 | commonRecyclerViewChecks(clazz, annotation) 94 | } else if (clazz.hasAnnotation(ANNOTATION_RECYCLERVIEWLIST)) { 95 | val annotation = clazz.getAnnotation(ANNOTATION_RECYCLERVIEWLIST) 96 | commonRecyclerViewChecks(clazz, annotation) 97 | 98 | // Detect usage of diffCallback method 99 | if (!clazz.allMethods.any { it.hasAnnotation(ANNOTATION_DIFFITEMSAME) }) { 100 | context.report( 101 | issue = ISSUE_NO_DIFFITEMSAME, 102 | scopeClass = clazz, 103 | location = context.getNameLocation(clazz), 104 | message = MESSAGE_ISSUE_NO_DIFFITEMSAME, 105 | quickfixData = createFixForInsertMethod( 106 | clazz = clazz, 107 | name = "Add \"DiffItemSame\" method", 108 | codeFix = "{\n@${ANNOTATION_DIFFITEMSAME}\nfun diffItemSame(${commonDiffParameters(clazz)}): Boolean { }" 109 | ) 110 | ) 111 | } 112 | if (!clazz.allMethods.any { it.hasAnnotation(ANNOTATION_DIFFCONTENTSAME) }) { 113 | context.report( 114 | issue = ISSUE_NO_DIFFCONTENTSAME, 115 | scopeClass = clazz, 116 | location = context.getNameLocation(clazz), 117 | message = MESSAGE_ISSUE_NO_DIFFCONTENTSAME, 118 | quickfixData = createFixForInsertMethod( 119 | clazz = clazz, 120 | name = "Add \"DiffContentSame\" method", 121 | codeFix = "{\n@${ANNOTATION_DIFFCONTENTSAME}\nfun diffContentSame(${commonDiffParameters(clazz)}): Boolean { }" 122 | ) 123 | ) 124 | } 125 | } 126 | } 127 | 128 | private fun commonRecyclerViewChecks(clazz: UClass, annotation: PsiAnnotation?) { 129 | if (annotation?.hasAttribute("dataSetType") == false) { 130 | context.report( 131 | issue = ISSUE_RECYCLERVIEW, 132 | scopeClass = clazz, 133 | location = context.getNameLocation(clazz), 134 | message = "Annotation must have a `dataSetType` parameter", 135 | quickfixData = createFixForDataSet(clazz, annotation) 136 | ) 137 | } 138 | 139 | // Detect usage of OnBind method 140 | if (clazz.allMethods.none { it.hasAnnotation(ANNOTATION_ONBIND) }) { 141 | context.report( 142 | issue = ISSUE_ON_BIND, 143 | scopeClass = clazz, 144 | location = context.getNameLocation(clazz), 145 | message = "There must be at least one @OnBind() method", 146 | quickfixData = createFixForInsertMethod( 147 | clazz = clazz, 148 | name = "Add \"OnBind\" method", 149 | codeFix = "{\n@${ANNOTATION_ONBIND}(layoutId = )\nfun bind($CORRECTED_BIND_PARAMETERS) { }" 150 | ) 151 | ) 152 | } 153 | } 154 | 155 | private fun reportInCorrectDiffContentSameAnnotation(node: UMethod) { 156 | context.report( 157 | issue = ISSUE_INCORRECT_DIFFCONTENTSAME, 158 | scope = node.context, 159 | location = context.getLocation(node), 160 | message = MESSAGE_ISSUE_INCORRECT_DIFFCONTENTSAME, 161 | quickfixData = commonCorrect("${node.name}(${commonDiffParameters(node.containingClass)}): Boolean {") 162 | ) 163 | } 164 | 165 | private fun reportInCorrectDiffItemSameAnnotation(node: UMethod) { 166 | context.report( 167 | issue = ISSUE_INCORRECT_DIFFITEMSAME, 168 | scope = node.context, 169 | location = context.getLocation(node), 170 | message = MESSAGE_ISSUE_INCORRECT_DIFFITEMSAME, 171 | quickfixData = commonCorrect("${node.name}(${commonDiffParameters(node.containingClass)}): Boolean {") 172 | ) 173 | } 174 | 175 | private fun reportInCorrectViewTypeAnnotation(node: UMethod) { 176 | context.report( 177 | issue = ISSUE_INCORRECT_ITEMVIEWTYPE, 178 | scope = node.context, 179 | location = context.getLocation(node), 180 | message = MESSAGE_ISSUE_INCORRECT_VIEWTYPE, 181 | quickfixData = commonCorrect("${node.name}(position: Int): Int {") 182 | ) 183 | } 184 | 185 | private fun reportInCorrectOnClickAnnotation(node: UMethod) { 186 | context.report( 187 | issue = ISSUE_INCORRECT_ONCLICK, 188 | scope = node.context, 189 | location = context.getLocation(node), 190 | message = MESSAGE_ISSUE_INCORRECT_ONCLICK, 191 | quickfixData = commonCorrect("${node.name}(context: android.content.Context, item: Any, position: Int) {") 192 | ) 193 | } 194 | 195 | private fun reportInCorrectOnLongClickAnnotation(node: UMethod) { 196 | context.report( 197 | issue = ISSUE_INCORRECT_ONLONGCLICK, 198 | scope = node.context, 199 | location = context.getLocation(node), 200 | message = MESSAGE_ISSUE_INCORRECT_ONLONGCLICK, 201 | quickfixData = commonCorrect("${node.name}(context: android.content.Context, item: Any, position: Int) {") 202 | ) 203 | } 204 | 205 | private fun reportInCorrectBindAnnotation(node: UMethod) { 206 | context.report( 207 | issue = ISSUE_INCORRECT_BIND, 208 | scope = node.context, 209 | location = context.getLocation(node), 210 | message = MESSAGE_ISSUE_INCORRECT_BIND, 211 | quickfixData = commonCorrect("${node.name}($CORRECTED_BIND_PARAMETERS) {") 212 | ) 213 | } 214 | 215 | private fun createFixForInsertMethod( 216 | clazz: UClass, 217 | name: String, 218 | codeFix: String 219 | ): LintFix { 220 | return LintFix.create() 221 | .replace() 222 | .name(name) 223 | .pattern("${Utils.getSimpleName(clazz.qualifiedName)}(.*)") 224 | .with(codeFix) 225 | .reformat(true) 226 | .shortenNames() 227 | .range( 228 | context.getRangeLocation( 229 | clazz.navigationElement, 230 | 0, 231 | clazz.textLength 232 | ) 233 | ) 234 | .build() 235 | } 236 | 237 | private fun createFixForDataSet( 238 | clazz: UClass, 239 | annotation: PsiAnnotation? 240 | ): LintFix { 241 | val fix = "(dataSetType = Any::class)" 242 | val className = Utils.getSimpleName(annotation?.qualifiedName) 243 | return LintFix.create() 244 | .replace() 245 | .name("Add \"dataSetType\" parameter") 246 | .pattern("$className(.*)") 247 | .with(fix) 248 | .range(context.getLocation(clazz.navigationElement)) 249 | .shortenNames() 250 | .build() 251 | } 252 | 253 | private fun commonCorrect(with: String): LintFix { 254 | return LintFix.create() 255 | .replace() 256 | .name("Add method parameters") 257 | .pattern(MATCHER_FUNCTION) 258 | .shortenNames() 259 | .reformat(true) 260 | .with(with) 261 | .robot(true) 262 | .independent(true) 263 | .build() 264 | } 265 | 266 | private fun commonDiffParameters(clazz: PsiClass?): String { 267 | val dataClass = Utils.findAnnotationValue(clazz, "dataSetType") 268 | .replace("::class", "") 269 | return "oldItem: $dataClass, newItem: $dataClass" 270 | } 271 | } 272 | 273 | companion object { 274 | const val CORRECTED_BIND_PARAMETERS = "view: android.view.View, item: Any, position: Int" 275 | 276 | const val MATCHER_FUNCTION = "fun ((.*?)\\{)" 277 | 278 | const val MESSAGE_ISSUE_NO_DIFFITEMSAME = "@DiffItemSame method is not implemented." 279 | const val MESSAGE_ISSUE_NO_DIFFCONTENTSAME = "@DiffContentSame method is not implemented." 280 | const val MESSAGE_ISSUE_INCORRECT_DIFFCONTENTSAME = 281 | "@DiffContentSame method is implemented incorrectly." 282 | const val MESSAGE_ISSUE_INCORRECT_DIFFITEMSAME = 283 | "@DiffItemSame method is implemented incorrectly." 284 | const val MESSAGE_ISSUE_INCORRECT_BIND = "@OnBind method is implemented incorrectly." 285 | const val MESSAGE_ISSUE_INCORRECT_VIEWTYPE = 286 | "@ItemViewType method is implemented incorrectly." 287 | const val MESSAGE_ISSUE_INCORRECT_ONCLICK = 288 | "@Onclick method parameters are implemented wrong." 289 | const val MESSAGE_ISSUE_INCORRECT_ONLONGCLICK = 290 | "@OnLongClick method parameters are implemented wrong." 291 | 292 | const val DETECTOR_INCORRECT_DIFFCALLBACK = "(.*?), (.*?)" 293 | const val DETECTOR_INCORRECT_ITEMVIEWTYPE = "int" 294 | const val DETECTOR_INCORRECT_BIND = "android.view.View, (.*?), int" 295 | const val DETECTOR_INCORRECT_CLICK = 296 | "android.content.Context, (.*?), int" 297 | 298 | const val ANNOTATION_RECYCLERVIEW = "com.kpstv.bindings.RecyclerViewAdapter" 299 | const val ANNOTATION_RECYCLERVIEWLIST = 300 | "com.kpstv.bindings.RecyclerViewListAdapter" 301 | const val ANNOTATION_DIFFITEMSAME = "com.kpstv.bindings.DiffItemSame" 302 | const val ANNOTATION_DIFFCONTENTSAME = "com.kpstv.bindings.DiffContentSame" 303 | const val ANNOTATION_ONCLICK = "com.kpstv.bindings.OnClick" 304 | const val ANNOTATION_ONLONGCLICK = "com.kpstv.bindings.OnLongClick" 305 | const val ANNOTATION_ONBIND = "com.kpstv.bindings.OnBind" 306 | const val ANNOTATION_ITEMVIEWTYPE = "com.kpstv.bindings.ItemViewType" 307 | 308 | private val IMPLEMENTATION = Implementation( 309 | RecyclerViewDetector::class.java, 310 | Scope.JAVA_FILE_SCOPE 311 | ) 312 | 313 | val ISSUE_ON_BIND: Issue = Issue 314 | .create( 315 | id = "onBindNeeded", 316 | briefDescription = "At least one @OnBind method must exist.", 317 | explanation = """ 318 | An adapter without the definition of OnBindViewHolder can lead to 319 | failure in compilation. 320 | """.trimIndent(), 321 | category = Category.CORRECTNESS, 322 | priority = 9, 323 | severity = Severity.WARNING, 324 | androidSpecific = true, 325 | implementation = IMPLEMENTATION 326 | ) 327 | val ISSUE_INCORRECT_BIND: Issue = Issue 328 | .create( 329 | id = "incorrectBindImplementation", 330 | briefDescription = MESSAGE_ISSUE_INCORRECT_BIND, 331 | explanation = """ 332 | The OnBind method is implemented incorrectly which can lead to 333 | failure in compilation. 334 | """.trimIndent(), 335 | category = Category.CORRECTNESS, 336 | priority = 10, 337 | severity = Severity.ERROR, 338 | androidSpecific = true, 339 | implementation = IMPLEMENTATION 340 | ) 341 | val ISSUE_RECYCLERVIEW: Issue = Issue.create( 342 | id = "recyclerViewParameterMissing", 343 | briefDescription = "No parameter provided for data class.", 344 | explanation = """ 345 | There must be a parameter for data class to be provided 346 | explicitly, otherwise it will be replaced with Any.class. 347 | """.trimIndent(), 348 | category = Category.CORRECTNESS, 349 | priority = 9, 350 | severity = Severity.WARNING, 351 | androidSpecific = true, 352 | implementation = IMPLEMENTATION 353 | ) 354 | val ISSUE_INCORRECT_ONCLICK: Issue = Issue.create( 355 | id = "incorrectOnClickImplementation", 356 | briefDescription = MESSAGE_ISSUE_INCORRECT_ONCLICK, 357 | explanation = """ 358 | The method is implemented incorrectly which can lead to 359 | failure in compilation. 360 | """.trimIndent(), 361 | category = Category.CORRECTNESS, 362 | priority = 10, 363 | severity = Severity.ERROR, 364 | androidSpecific = true, 365 | implementation = IMPLEMENTATION 366 | ) 367 | val ISSUE_INCORRECT_ONLONGCLICK: Issue = Issue.create( 368 | id = "incorrectOnLongClickImplementation", 369 | briefDescription = MESSAGE_ISSUE_INCORRECT_ONLONGCLICK, 370 | explanation = """ 371 | The method is implemented incorrectly which can lead to 372 | failure in compilation. 373 | """.trimIndent(), 374 | category = Category.CORRECTNESS, 375 | priority = 10, 376 | severity = Severity.ERROR, 377 | androidSpecific = true, 378 | implementation = IMPLEMENTATION 379 | ) 380 | 381 | val ISSUE_INCORRECT_ITEMVIEWTYPE: Issue = Issue.create( 382 | id = "incorrectItemViewTypeImplementation", 383 | briefDescription = MESSAGE_ISSUE_INCORRECT_VIEWTYPE, 384 | explanation = """ 385 | The method is implemented incorrectly which can lead to 386 | failure in compilation. 387 | """.trimIndent(), 388 | category = Category.CORRECTNESS, 389 | priority = 10, 390 | severity = Severity.ERROR, 391 | androidSpecific = true, 392 | implementation = IMPLEMENTATION 393 | ) 394 | 395 | val ISSUE_INCORRECT_DIFFITEMSAME: Issue = Issue.create( 396 | id = "incorrectDiffItemSameCallback", 397 | briefDescription = MESSAGE_ISSUE_INCORRECT_DIFFITEMSAME, 398 | explanation = """ 399 | The method is implemented incorrectly which can lead to 400 | failure in compilation. 401 | """.trimIndent(), 402 | category = Category.CORRECTNESS, 403 | priority = 10, 404 | severity = Severity.ERROR, 405 | androidSpecific = true, 406 | implementation = IMPLEMENTATION 407 | ) 408 | val ISSUE_INCORRECT_DIFFCONTENTSAME: Issue = Issue.create( 409 | id = "incorrectDiffItemContentCallback", 410 | briefDescription = MESSAGE_ISSUE_INCORRECT_DIFFCONTENTSAME, 411 | explanation = """ 412 | The method is implemented incorrectly which can lead to 413 | failure in compilation. 414 | """.trimIndent(), 415 | category = Category.CORRECTNESS, 416 | priority = 10, 417 | severity = Severity.ERROR, 418 | androidSpecific = true, 419 | implementation = IMPLEMENTATION 420 | ) 421 | 422 | val ISSUE_NO_DIFFITEMSAME: Issue = Issue.create( 423 | id = "noDiffItemSameCallback", 424 | briefDescription = MESSAGE_ISSUE_NO_DIFFITEMSAME, 425 | explanation = """ 426 | The method is not implemented which can lead to 427 | failure in compilation. 428 | """.trimIndent(), 429 | category = Category.CORRECTNESS, 430 | priority = 10, 431 | severity = Severity.ERROR, 432 | androidSpecific = true, 433 | implementation = IMPLEMENTATION 434 | ) 435 | val ISSUE_NO_DIFFCONTENTSAME: Issue = Issue.create( 436 | id = "noDiffContentSameCallback", 437 | briefDescription = MESSAGE_ISSUE_NO_DIFFCONTENTSAME, 438 | explanation = """ 439 | The method is not implemented which can lead to 440 | failure in compilation. 441 | """.trimIndent(), 442 | category = Category.CORRECTNESS, 443 | priority = 10, 444 | severity = Severity.ERROR, 445 | androidSpecific = true, 446 | implementation = IMPLEMENTATION 447 | ) 448 | } 449 | } --------------------------------------------------------------------------------