diff --git a/buildSrc/buildSrc/src/main/kotlin/Deps.kt b/buildSrc/buildSrc/src/main/kotlin/Deps.kt index 3b1e525..e666cd1 100644 --- a/buildSrc/buildSrc/src/main/kotlin/Deps.kt +++ b/buildSrc/buildSrc/src/main/kotlin/Deps.kt @@ -26,6 +26,14 @@ object Deps { const val compiler = "com.github.stephanenicolas.toothpick:toothpick-compiler:${Versions.toothpick}" } + object lint { + const val core = "com.android.tools.lint:lint:${Versions.androidTools}" + const val api = "com.android.tools.lint:lint-api:${Versions.androidTools}" + const val checks = "com.android.tools.lint:lint-checks:${Versions.androidTools}" + const val tests = "com.android.tools.lint:lint-tests:${Versions.androidTools}" + } + const val testutils = "com.android.tools:testutils:${Versions.androidTools}" + const val cicerone = "ru.terrakok.cicerone:cicerone:${Versions.cicerone}" const val timber = "com.jakewharton.timber:timber:${Versions.timber}" diff --git a/buildSrc/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/buildSrc/src/main/kotlin/Versions.kt index 9b90d89..a8b18c5 100644 --- a/buildSrc/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/buildSrc/src/main/kotlin/Versions.kt @@ -30,4 +30,6 @@ object Versions { const val bintray = "1.8.5" const val ktlint = "0.39.0" + + const val androidTools = "27.1.2" } diff --git a/gemini-lint/build.gradle.kts b/gemini-lint/build.gradle.kts new file mode 100644 index 0000000..44e237b --- /dev/null +++ b/gemini-lint/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("java-library") + id("kotlin") +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + compileOnly(Deps.lint.api) + compileOnly(Deps.lint.checks) + + testImplementation(Deps.kotlinTestJunit) + testImplementation(Deps.lint.core) + testImplementation(Deps.lint.tests) + testImplementation(Deps.testutils) +} + +tasks.jar { + manifest { + attributes( + "Lint-Registry-v3" to "com.haroncode.gemini.lint.GenimiIssueRegistry" + ) + } +} diff --git a/gemini-lint/src/main/java/com/haroncode/gemini/lint/GenimiIssueRegistry.kt b/gemini-lint/src/main/java/com/haroncode/gemini/lint/GenimiIssueRegistry.kt new file mode 100644 index 0000000..fc79dd9 --- /dev/null +++ b/gemini-lint/src/main/java/com/haroncode/gemini/lint/GenimiIssueRegistry.kt @@ -0,0 +1,11 @@ +package com.haroncode.gemini.lint + +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.detector.api.CURRENT_API +import com.android.tools.lint.detector.api.Issue + +class GenimiIssueRegistry : IssueRegistry() { + override val issues = WrongGenimiUsageDetector.issues + + override val api = CURRENT_API +} diff --git a/gemini-lint/src/main/java/com/haroncode/gemini/lint/WrongGenimiUsageDetector.kt b/gemini-lint/src/main/java/com/haroncode/gemini/lint/WrongGenimiUsageDetector.kt new file mode 100644 index 0000000..0f0dda4 --- /dev/null +++ b/gemini-lint/src/main/java/com/haroncode/gemini/lint/WrongGenimiUsageDetector.kt @@ -0,0 +1,49 @@ +@file:Suppress("UnstableApiUsage") + +package com.haroncode.gemini.lint + +import com.android.tools.lint.detector.api.* +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression + +class WrongGenimiUsageDetector : Detector(), SourceCodeScanner { + override fun getApplicableMethodNames() = listOf("bind") + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + val evaluator = context.evaluator + + if (evaluator.isMemberInSubClassOf(method, "com.haroncode.gemini.binder.Binder")) { + checkCallMethod(method, node, context) + } + } + + + + private fun checkCallMethod(method: PsiMethod, call: UCallExpression, context: JavaContext) { + var parent = call.uastParent + while (parent != null && parent !is PsiMethod) { + parent = parent.uastParent + } + if (parent !is PsiMethod) return + if (parent.name != "onCreate") { + context.report(ISSUE_ON_CREATE, method, context.getLocation(call), "Calling bind from ${parent.name} instead of onCreate") + } + } + + companion object { + val ISSUE_ON_CREATE = Issue.create( + "ShouldBeCalledInOnCreate", + "Bind method for ViewStoreBinding should be called in onCreate method", + "Based on documentation you should call bind in the onCreate method of your's view", + Category.CORRECTNESS, + 5, + Severity.ERROR, + Implementation(WrongGenimiUsageDetector::class.java, Scope.JAVA_FILE_SCOPE) + ) + + val issues = listOf( + ISSUE_ON_CREATE + ) + } + +} diff --git a/gemini-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/gemini-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry new file mode 100644 index 0000000..4fb3f6e --- /dev/null +++ b/gemini-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry @@ -0,0 +1 @@ +com.haroncode.gemini.lint.GenimiIssueRegistry \ No newline at end of file diff --git a/gemini-lint/src/test/java/com/haroncode/gemini/lint/WrongGenimiUsageDetectorTest.kt b/gemini-lint/src/test/java/com/haroncode/gemini/lint/WrongGenimiUsageDetectorTest.kt new file mode 100644 index 0000000..9f8b196 --- /dev/null +++ b/gemini-lint/src/test/java/com/haroncode/gemini/lint/WrongGenimiUsageDetectorTest.kt @@ -0,0 +1,159 @@ +package com.haroncode.gemini.lint + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest + +@Suppress("UnstableApiUsage") +class WrongGenimiUsageDetectorTest : LintDetectorTest() { + companion object { + private val GEMINI_STUB1 = kotlin(BINDER_STUB) + private val GEMINI_STUB2 = kotlin(BINDER_RULES) + private val OTHER_STUB = kotlin(OTHER_BINDER_STUB) + } + + fun testRightUsageWithActivity() { + lint() + .files( + GEMINI_STUB1, + GEMINI_STUB2, + kotlin(""" + package ru.test.app + + import android.app.Activity + import com.haroncode.gemini.binder.* + import com.haroncode.gemini.binder.rule.* + + class Activity1 : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + StoreViewBinding.with(generateSampleRulesFactory()) + .bind(this) + } + } + """.trimIndent()) + ) + .requireCompileSdk() + .run() + .expectClean() + } + + fun testRightUsageWithFragment() { + lint() + .files( + GEMINI_STUB1, + GEMINI_STUB2, + kotlin(""" + package ru.test.app + + import android.app.Fragment + import com.haroncode.gemini.binder.* + import com.haroncode.gemini.binder.rule.* + + class Fragment1 : Fragment() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + StoreViewBinding.with(generateSampleRulesFactory()) + .bind(this) + } + } + """.trimIndent()) + ) + .requireCompileSdk() + .run() + .expectClean() + } + + fun testWrongUsageWithFragment_onViewCreated() { + lint() + .files( + GEMINI_STUB1, + GEMINI_STUB2, + kotlin(""" + package ru.test.app + + import android.app.Fragment + import com.haroncode.gemini.binder.* + import com.haroncode.gemini.binder.rule.* + + class Fragment1 : Fragment() { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + StoreViewBinding.with(generateSampleRulesFactory()) + .bind(this) + } + } + """.trimIndent()) + ) + .requireCompileSdk() + .run() + .expect(""" + src/ru/test/app/Fragment1.kt:11: Error: Calling bind from onViewCreated instead of onCreate [ShouldBeCalledInOnCreate] + StoreViewBinding.with(generateSampleRulesFactory()) + ^ + 1 errors, 0 warnings + """.trimIndent()) + } + + fun testWrongUsageWithActivity_onResume() { + lint() + .files( + GEMINI_STUB1, + GEMINI_STUB2, + kotlin(""" + package ru.test.app + + import android.app.Activity + import com.haroncode.gemini.binder.* + import com.haroncode.gemini.binder.rule.* + + class Activity1 : Activity() { + override fun onResume() { + super.onViewCreated(view, savedInstanceState) + + StoreViewBinding.with(generateSampleRulesFactory()) + .bind(this) + } + } + """.trimIndent()) + ) + .requireCompileSdk() + .run() + .expect(""" + src/ru/test/app/Activity1.kt:11: Error: Calling bind from onResume instead of onCreate [ShouldBeCalledInOnCreate] + StoreViewBinding.with(generateSampleRulesFactory()) + ^ + 1 errors, 0 warnings + """.trimIndent()) + } + + fun testDoesNotTriggeredByOtherClasses() { + lint() + .files( + OTHER_STUB, + kotlin(""" + package ru.test.app + + import android.app.Fragment + import ru.test.lib.* + + class Fragment1 : Fragment() { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + StoreViewBinding.with().bind(this) + } + } + """.trimIndent()) + ) + .requireCompileSdk() + .run() + .expectClean() + + } + + override fun getDetector() = WrongGenimiUsageDetector() + + override fun getIssues() = WrongGenimiUsageDetector.issues +} diff --git a/gemini-lint/src/test/java/com/haroncode/gemini/lint/stubs.kt b/gemini-lint/src/test/java/com/haroncode/gemini/lint/stubs.kt new file mode 100644 index 0000000..1e39fae --- /dev/null +++ b/gemini-lint/src/test/java/com/haroncode/gemini/lint/stubs.kt @@ -0,0 +1,74 @@ +package com.haroncode.gemini.lint + +val BINDER_STUB = """ +package com.haroncode.gemini.binder + +import com.haroncode.gemini.binder.rule.* + +object StoreViewBinding { + fun with( + lifecycleStrategy: LifecycleStrategy = StartStopStrategy, + factoryProvider: () -> BindingRulesFactory, + ): Binder = SimpleBinder( + factoryProvider = factoryProvider, + lifecycleStrategy = lifecycleStrategy, + ) + fun withRestore( + lifecycleStrategy: LifecycleStrategy = StartStopStrategy, + factoryProvider: () -> BindingRulesFactory, + ): Binder = RestoreBinder( + factoryProvider = factoryProvider, + lifecycleStrategy = lifecycleStrategy, + ) + fun with( + factory: BindingRulesFactory, + lifecycleStrategy: LifecycleStrategy = StartStopStrategy + ): Binder = BinderImpl( + factory = factory, + lifecycleStrategy = lifecycleStrategy + ) +} + +interface Binder { + fun bind(view: View) +} + +internal class BinderImpl( + private val factory: BindingRulesFactory, + private val lifecycleStrategy: LifecycleStrategy +) : Binder { + override fun bind(view: View) { } +} +""".trimIndent() + +val BINDER_RULES = """ +package com.haroncode.gemini.binder.rule + +interface BindingRulesFactory

{ + fun create(param: P): Collection +} + +interface BindingRule { + suspend fun bind() +} + +fun generateSampleRulesFactory() = object : BindingRulesFactory { + override fun create(param: T): Collection = emptyList() +} +""".trimIndent() + +val OTHER_BINDER_STUB = """ +package ru.test.lib + +object StoreViewBinding { + fun with(): Binder = BinderImpl() +} + +interface Binder { + fun bind(view: View) +} + +class BinderImpl : Binder { + override fun bind(view: View) {} +} +""".trimIndent() diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index dc8198e..23580b7 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -52,4 +52,6 @@ dependencies { implementation(Deps.timber) implementation(project(":gemini-binder")) + + lintChecks(project(":gemini-lint")) } diff --git a/settings.gradle.kts b/settings.gradle.kts index f35c7a4..fab7ed6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,4 @@ +include(":gemini-lint") include( ":gemini-core", ":gemini-core-test",