Skip to content

Commit

Permalink
Merge pull request #1888 from Adyen/chore/dispatchers-lint-rule
Browse files Browse the repository at this point in the history
Add lint rule that prevents Dispatchers usage
  • Loading branch information
OscarSpruit authored Nov 14, 2024
2 parents 2adfd4d + 454a597 commit 357cf03
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ internal class LintIssueRegistry : IssueRegistry() {

override val issues: List<Issue> = 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,
)
}
Original file line number Diff line number Diff line change
@@ -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(),
),
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down

0 comments on commit 357cf03

Please sign in to comment.