diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/DispatcherProvider.kt b/checkout-core/src/main/java/com/adyen/checkout/core/DispatcherProvider.kt index 090c7b96e0..44ebbcab28 100644 --- a/checkout-core/src/main/java/com/adyen/checkout/core/DispatcherProvider.kt +++ b/checkout-core/src/main/java/com/adyen/checkout/core/DispatcherProvider.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainCoroutineDispatcher +@Suppress("NotDispatcherProvider") @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) object DispatcherProvider { diff --git a/lint/src/main/java/com/adyen/checkout/lint/LintIssueRegistry.kt b/lint/src/main/java/com/adyen/checkout/lint/LintIssueRegistry.kt index 7597f92cf5..837af8aa34 100644 --- a/lint/src/main/java/com/adyen/checkout/lint/LintIssueRegistry.kt +++ b/lint/src/main/java/com/adyen/checkout/lint/LintIssueRegistry.kt @@ -19,9 +19,10 @@ internal class LintIssueRegistry : IssueRegistry() { override val issues: List = listOf( CONTEXT_GET_STRING_ISSUE, + JSON_OPT_FUNCTIONS_ISSUE, NOT_ADYEN_LOG_ISSUE, + NOT_DISPATCHER_PROVIDER_ISSUE, OBJECT_IN_PUBLIC_SEALED_CLASS_ISSUE, TEXT_IN_LAYOUT_XML_ISSUE, - JSON_OPT_FUNCTIONS_ISSUE, ) } diff --git a/lint/src/main/java/com/adyen/checkout/lint/NotDispatcherProvider.kt b/lint/src/main/java/com/adyen/checkout/lint/NotDispatcherProvider.kt new file mode 100644 index 0000000000..73359cdd29 --- /dev/null +++ b/lint/src/main/java/com/adyen/checkout/lint/NotDispatcherProvider.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 8/11/2024. + */ + +package com.adyen.checkout.lint + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiElement +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.getQualifiedName + +internal val NOT_DISPATCHER_PROVIDER_ISSUE = Issue.create( + id = "NotDispatcherProvider", + briefDescription = "Dispatchers used instead of DispatcherProvider", + explanation = "DispatcherProvider should be used, so we can override dispatchers for testing purposes.", + implementation = Implementation(NotDispatcherProvider::class.java, Scope.JAVA_FILE_SCOPE), + category = Category.CUSTOM_LINT_CHECKS, + priority = 7, + severity = Severity.ERROR, +) + +internal class NotDispatcherProvider : Detector(), SourceCodeScanner { + + override fun getApplicableReferenceNames() = listOf( + "Dispatchers", + ) + + override fun visitReference(context: JavaContext, reference: UReferenceExpression, referenced: PsiElement) { + if (reference.getQualifiedName() == "kotlinx.coroutines.Dispatchers") { + context.report( + NOT_DISPATCHER_PROVIDER_ISSUE, + reference, + context.getLocation(reference), + "Dispatchers used instead of DispatcherProvider", + fix().alternatives( + fix() + .replace() + .with("DispatcherProvider") + .imports("com.adyen.checkout.core.DispatcherProvider") + .reformat(true) + .shortenNames() + .build(), + ), + ) + } + } +} diff --git a/lint/src/test/java/com/adyen/checkout/lint/NotDispatcherProviderTest.kt b/lint/src/test/java/com/adyen/checkout/lint/NotDispatcherProviderTest.kt new file mode 100644 index 0000000000..a4575ecb9b --- /dev/null +++ b/lint/src/test/java/com/adyen/checkout/lint/NotDispatcherProviderTest.kt @@ -0,0 +1,95 @@ +package com.adyen.checkout.lint + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest.kotlin +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint +import org.junit.Test + +internal class NotDispatcherProviderTest { + + @Test + fun whenDispatchersIsUsed_thenIssueIsDetected() { + lint() + .files( + DISPATCHERS_STUB, + DISPATCHER_PROVIDER_STUB, + kotlin( + """ + package test + + import kotlinx.coroutines.Dispatchers + import kotlinx.coroutines.CoroutineDispatcher + import com.adyen.checkout.core.DispatcherProvider + + class Test( + private val dispatcher: CoroutineDispatcher = Dispatchers.IO + ) { + + fun test() = withContext(Dispatchers.Default) { + withContext(DispatcherProvider.Main) {} + } + } + """, + ).indented(), + ) + .issues(NOT_DISPATCHER_PROVIDER_ISSUE) + .allowMissingSdk() + .allowDuplicates() + .run() + .expect( + """ + src/test/Test.kt:8: Error: Dispatchers used instead of DispatcherProvider [NotDispatcherProvider] + private val dispatcher: CoroutineDispatcher = Dispatchers.IO + ~~~~~~~~~~~ + src/test/Test.kt:11: Error: Dispatchers used instead of DispatcherProvider [NotDispatcherProvider] + fun test() = withContext(Dispatchers.Default) { + ~~~~~~~~~~~ + 2 errors, 0 warnings + """, + ) + .expectFixDiffs( + """ + Fix for src/test/Test.kt line 8: Replace with DispatcherProvider: + @@ -8 +8 + - private val dispatcher: CoroutineDispatcher = Dispatchers.IO + + private val dispatcher: CoroutineDispatcher = DispatcherProvider.IO + Fix for src/test/Test.kt line 11: Replace with DispatcherProvider: + @@ -11 +11 + - fun test() = withContext(Dispatchers.Default) { + + fun test() = withContext(DispatcherProvider.Default) { + """, + ) + } + + companion object { + + private val DISPATCHERS_STUB = kotlin( + """ + package kotlinx.coroutines + + object Dispatchers { + val Main = CoroutineDispatcher() + val Default = CoroutineDispatcher() + val IO = CoroutineDispatcher() + } + + open class CoroutineDispatcher + + fun withContext(dispatcher: CoroutineDispatcher, block: () -> Unit) { + block() + } + """, + ).indented() + + private val DISPATCHER_PROVIDER_STUB = kotlin( + """ + package com.adyen.checkout.core + + object DispatcherProvider { + val Main = CoroutineDispatcher() + val Default = CoroutineDispatcher() + val IO = CoroutineDispatcher() + } + """, + ).indented() + } +} diff --git a/test-core/src/testFixtures/java/com/adyen/checkout/test/TestDispatcherExtension.kt b/test-core/src/testFixtures/java/com/adyen/checkout/test/TestDispatcherExtension.kt index 8df09af10a..8d04084825 100644 --- a/test-core/src/testFixtures/java/com/adyen/checkout/test/TestDispatcherExtension.kt +++ b/test-core/src/testFixtures/java/com/adyen/checkout/test/TestDispatcherExtension.kt @@ -21,6 +21,7 @@ import org.junit.jupiter.api.extension.ExtensionContext * JUnit 5 extension that replaces [Dispatchers.Main] with a test dispatcher. This gives control over how the dispatcher * executes it's work. */ +@Suppress("NotDispatcherProvider") @OptIn(ExperimentalCoroutinesApi::class) class TestDispatcherExtension : BeforeEachCallback, AfterEachCallback {