Skip to content

Commit c39f91d

Browse files
authored
Add lint check support for kotlin extensions (#439)
1 parent e37f77d commit c39f91d

File tree

3 files changed

+122
-5
lines changed

3 files changed

+122
-5
lines changed

docs/lint-check.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ autodispose.lenient=true
6161

6262
The default value of this is `false`.
6363

64+
### Kotlin Extension
65+
66+
By default, `subscribe` and `subscribeWith` methods are checked. To support other subscribe methods such as `subscribeBy` in RxKotlin, you can add your own subscribe extensions.
67+
In your **app-level** `gradle.properties` files, add kotlin extension functions in format of `{full package name for extension's scope}#{functionName}` and comma-separated-values like so:
68+
69+
```properties
70+
autodispose.kotlinExtensionFunctions="io.reactivex.rxjava3.kotlin.subscribers#subscribeBy,com.sample.app.SubscribeExt#subscribe2"
71+
```
72+
6473
#### Examples
6574
```java
6675
// This is allowed in lenient mode
@@ -77,4 +86,7 @@ Observable.just(1).subscribe();
7786

7887
// This is not allowed in lenient mode, because that subscribe() overload just returns void
7988
Observable.just(1).subscribe(new Observer...)
89+
90+
// This is not allowed when kotlin extension functions option is used
91+
Observable.just(1).subscribeBy { }
8092
```

static-analysis/autodispose-lint/src/main/kotlin/autodispose2/lint/AutoDisposeDetector.kt

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import com.android.tools.lint.detector.api.JavaContext
2626
import com.android.tools.lint.detector.api.Scope
2727
import com.android.tools.lint.detector.api.Severity
2828
import com.android.tools.lint.detector.api.SourceCodeScanner
29+
import com.google.common.collect.HashMultimap
30+
import com.google.common.collect.Multimap
2931
import com.intellij.psi.PsiMethod
3032
import com.intellij.psi.PsiType
3133
import com.intellij.psi.util.PsiUtil
@@ -48,6 +50,7 @@ import org.jetbrains.uast.visitor.AbstractUastVisitor
4850
internal const val CUSTOM_SCOPE_KEY = "autodispose.typesWithScope"
4951
internal const val LENIENT = "autodispose.lenient"
5052
internal const val OVERRIDE_SCOPES = "autodispose.overrideScopes"
53+
internal const val KOTLIN_EXTENSION_FUNCTIONS = "autodispose.kotlinExtensionFunctions"
5154

5255
/**
5356
* Detector which checks if your stream subscriptions are handled by AutoDispose.
@@ -98,19 +101,24 @@ class AutoDisposeDetector : Detector(), SourceCodeScanner {
98101
private val REACTIVE_TYPES = setOf(OBSERVABLE, FLOWABLE, PARALLEL_FLOWABLE, SINGLE, MAYBE,
99102
COMPLETABLE)
100103

104+
private val REACTIVE_SUBSCRIBE_METHOD_NAMES = setOf("subscribe", "subscribeWith")
105+
101106
internal const val PROPERTY_FILE = "gradle.properties"
102107
}
103108

104109
// The scopes that are applicable for the lint check.
105110
// This includes the DEFAULT_SCOPES as well as any custom scopes
106111
// defined by the consumer.
107112
private lateinit var appliedScopes: Set<String>
113+
private lateinit var ktExtensionMethodToPackageMap: Multimap<String, String>
114+
private lateinit var appliedMethodNames: List<String>
108115

109116
private var lenient: Boolean = false
110117

111118
override fun beforeCheckRootProject(context: Context) {
112119
var overrideScopes = false
113120
val scopes = mutableSetOf<String>()
121+
val ktExtensionMethodToPackageMap = HashMultimap.create<String, String>()
114122

115123
// Add the custom scopes defined in configuration.
116124
val props = Properties()
@@ -125,6 +133,16 @@ class AutoDisposeDetector : Detector(), SourceCodeScanner {
125133
.toList()
126134
scopes.addAll(customScopes)
127135
}
136+
props.getProperty(KOTLIN_EXTENSION_FUNCTIONS)?.let { ktExtensionProperty ->
137+
ktExtensionProperty.split(",")
138+
.forEach {
139+
val arr = it.split("#", limit = 2)
140+
if (arr.size >= 2) {
141+
val (packageName, methodName) = arr
142+
ktExtensionMethodToPackageMap.put(methodName, packageName)
143+
}
144+
}
145+
}
128146
props.getProperty(LENIENT)?.toBoolean()?.let {
129147
lenient = it
130148
}
@@ -136,10 +154,12 @@ class AutoDisposeDetector : Detector(), SourceCodeScanner {
136154
if (!overrideScopes) {
137155
scopes.addAll(DEFAULT_SCOPES)
138156
}
139-
appliedScopes = scopes
157+
this.appliedScopes = scopes
158+
this.ktExtensionMethodToPackageMap = ktExtensionMethodToPackageMap
159+
this.appliedMethodNames = (REACTIVE_SUBSCRIBE_METHOD_NAMES + ktExtensionMethodToPackageMap.keySet()).toList()
140160
}
141161

142-
override fun getApplicableMethodNames(): List<String> = listOf("subscribe", "subscribeWith")
162+
override fun getApplicableMethodNames(): List<String> = appliedMethodNames
143163

144164
override fun createUastHandler(context: JavaContext): UElementHandler? {
145165
return object : UElementHandler() {
@@ -257,7 +277,15 @@ class AutoDisposeDetector : Detector(), SourceCodeScanner {
257277
}
258278

259279
private fun isReactiveType(evaluator: JavaEvaluator, method: PsiMethod): Boolean {
260-
return REACTIVE_TYPES.any { evaluator.isMemberInClass(method, it) }
280+
return REACTIVE_SUBSCRIBE_METHOD_NAMES.contains(method.name) && REACTIVE_TYPES.any {
281+
evaluator.isMemberInClass(method, it)
282+
}
283+
}
284+
285+
private fun isKotlinExtension(evaluator: JavaEvaluator, method: PsiMethod): Boolean {
286+
return ktExtensionMethodToPackageMap.get(method.name).any {
287+
evaluator.isMemberInClass(method, it)
288+
}
261289
}
262290

263291
/**
@@ -295,7 +323,7 @@ class AutoDisposeDetector : Detector(), SourceCodeScanner {
295323
if (!getApplicableMethodNames().contains(method.name)) return
296324
val evaluator = context.evaluator
297325

298-
if (isReactiveType(evaluator, method) &&
326+
if ((isReactiveType(evaluator, method) || isKotlinExtension(evaluator, method)) &&
299327
isInScope(evaluator, node)
300328
) {
301329
if (!lenient) {

static-analysis/autodispose-lint/src/test/kotlin/autodispose2/lint/AutoDisposeDetectorTest.kt

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,33 @@ class AutoDisposeDetectorTest {
9999
}
100100
""").indented().within("src/")
101101

102-
private fun lenientPropertiesFile(lenient: Boolean = true): TestFile.PropertyTestFile {
102+
private val RX_KOTLIN = kotlin(
103+
"io/reactivex/rxjava3/kotlin/subscribers.kt",
104+
"""
105+
@file:JvmName("subscribers")
106+
package io.reactivex.rxjava3.kotlin
107+
108+
import io.reactivex.rxjava3.core.*
109+
import io.reactivex.rxjava3.disposables.Disposable
110+
111+
fun <G : Any> Observable<G>.subscribeBy(
112+
onNext: (G) -> Unit
113+
): Disposable = subscribe()
114+
""").indented().within("src/")
115+
116+
private fun propertiesFile(lenient: Boolean = true, kotlinExtensionFunctions: String? = null): TestFile.PropertyTestFile {
103117
val properties = projectProperties()
104118
properties.property(LENIENT, lenient.toString())
119+
kotlinExtensionFunctions?.also {
120+
properties.property(KOTLIN_EXTENSION_FUNCTIONS, it)
121+
}
105122
properties.to(AutoDisposeDetector.PROPERTY_FILE)
106123
return properties
107124
}
125+
126+
private fun lenientPropertiesFile(lenient: Boolean = true): TestFile.PropertyTestFile {
127+
return propertiesFile(lenient)
128+
}
108129
}
109130

110131
@Test fun observableErrorsOutOnOmittingAutoDispose() {
@@ -1002,6 +1023,62 @@ class AutoDisposeDetectorTest {
10021023
.expectClean()
10031024
}
10041025

1026+
@Test fun kotlinExtensionFunctionNotConfigured() {
1027+
lint()
1028+
.files(rxJava3(),
1029+
LIFECYCLE_OWNER,
1030+
RX_KOTLIN,
1031+
FRAGMENT,
1032+
kotlin("""
1033+
package foo
1034+
import io.reactivex.rxjava3.core.Observable
1035+
import io.reactivex.rxjava3.observers.DisposableObserver
1036+
import io.reactivex.rxjava3.kotlin.subscribeBy
1037+
import androidx.fragment.app.Fragment
1038+
1039+
class ExampleClass : Fragment() {
1040+
fun names() {
1041+
val obs = Observable.just(1, 2, 3, 4)
1042+
obs.subscribeBy { }
1043+
}
1044+
}
1045+
""").indented())
1046+
.allowCompilationErrors(false)
1047+
.issues(AutoDisposeDetector.ISSUE)
1048+
.run()
1049+
.expectClean()
1050+
}
1051+
1052+
@Test fun kotlinExtensionFunctionNotHandled() {
1053+
lint()
1054+
.files(rxJava3(),
1055+
propertiesFile(kotlinExtensionFunctions = "io.reactivex.rxjava3.kotlin.subscribers#subscribeBy"),
1056+
LIFECYCLE_OWNER,
1057+
RX_KOTLIN,
1058+
FRAGMENT,
1059+
kotlin("""
1060+
package foo
1061+
import io.reactivex.rxjava3.core.Observable
1062+
import io.reactivex.rxjava3.observers.DisposableObserver
1063+
import io.reactivex.rxjava3.kotlin.subscribeBy
1064+
import androidx.fragment.app.Fragment
1065+
1066+
class ExampleClass : Fragment() {
1067+
fun names() {
1068+
val obs = Observable.just(1, 2, 3, 4)
1069+
obs.subscribeBy { }
1070+
}
1071+
}
1072+
""").indented())
1073+
.allowCompilationErrors(false)
1074+
.issues(AutoDisposeDetector.ISSUE)
1075+
.run()
1076+
.expect("""src/foo/ExampleClass.kt:10: Error: ${AutoDisposeDetector.LINT_DESCRIPTION} [AutoDispose]
1077+
| obs.subscribeBy { }
1078+
| ~~~~~~~~~~~~~~~~~~~
1079+
|1 errors, 0 warnings""".trimMargin())
1080+
}
1081+
10051082
@Test fun subscribeWithCapturedNonDisposableType() {
10061083
lint()
10071084
.files(rxJava3(),

0 commit comments

Comments
 (0)