David Liu

Flow Delegate usage detector

@@ -16,6 +16,7 @@ import io.livekit.android.events.RoomEvent @@ -16,6 +16,7 @@ import io.livekit.android.events.RoomEvent
16 import io.livekit.android.renderer.TextureViewRenderer 16 import io.livekit.android.renderer.TextureViewRenderer
17 import io.livekit.android.room.participant.* 17 import io.livekit.android.room.participant.*
18 import io.livekit.android.room.track.* 18 import io.livekit.android.room.track.*
  19 +import io.livekit.android.util.FlowObservable
19 import io.livekit.android.util.LKLog 20 import io.livekit.android.util.LKLog
20 import io.livekit.android.util.flow 21 import io.livekit.android.util.flow
21 import io.livekit.android.util.flowDelegate 22 import io.livekit.android.util.flowDelegate
@@ -60,23 +61,18 @@ constructor( @@ -60,23 +61,18 @@ constructor(
60 @Deprecated("Use events instead.") 61 @Deprecated("Use events instead.")
61 var listener: RoomListener? = null 62 var listener: RoomListener? = null
62 63
63 - /**  
64 - * Changes can be observed by using [io.livekit.android.util.flow]  
65 - */ 64 + @get:FlowObservable
66 var sid: Sid? by flowDelegate(null) 65 var sid: Sid? by flowDelegate(null)
67 - /**  
68 - * Changes can be observed by using [io.livekit.android.util.flow]  
69 - */ 66 +
  67 + @get:FlowObservable
70 var name: String? by flowDelegate(null) 68 var name: String? by flowDelegate(null)
71 private set 69 private set
72 - /**  
73 - * Changes can be observed by using [io.livekit.android.util.flow]  
74 - */ 70 +
  71 + @get:FlowObservable
75 var state: State by flowDelegate(State.DISCONNECTED) 72 var state: State by flowDelegate(State.DISCONNECTED)
76 private set 73 private set
77 - /**  
78 - * Changes can be observed by using [io.livekit.android.util.flow]  
79 - */ 74 +
  75 + @get:FlowObservable
80 var metadata: String? by flowDelegate(null) 76 var metadata: String? by flowDelegate(null)
81 private set 77 private set
82 78
@@ -88,17 +84,16 @@ constructor( @@ -88,17 +84,16 @@ constructor(
88 84
89 lateinit var localParticipant: LocalParticipant 85 lateinit var localParticipant: LocalParticipant
90 private set 86 private set
  87 +
91 private var mutableRemoteParticipants by flowDelegate(emptyMap<String, RemoteParticipant>()) 88 private var mutableRemoteParticipants by flowDelegate(emptyMap<String, RemoteParticipant>())
92 - /**  
93 - * Changes can be observed by using [io.livekit.android.util.flow]  
94 - */ 89 +
  90 + @get:FlowObservable
95 val remoteParticipants: Map<String, RemoteParticipant> 91 val remoteParticipants: Map<String, RemoteParticipant>
96 get() = mutableRemoteParticipants 92 get() = mutableRemoteParticipants
97 93
98 private var mutableActiveSpeakers by flowDelegate(emptyList<Participant>()) 94 private var mutableActiveSpeakers by flowDelegate(emptyList<Participant>())
99 - /**  
100 - * Changes can be observed by using [io.livekit.android.util.flow]  
101 - */ 95 +
  96 + @get:FlowObservable
102 val activeSpeakers: List<Participant> 97 val activeSpeakers: List<Participant>
103 get() = mutableActiveSpeakers 98 get() = mutableActiveSpeakers
104 99
@@ -109,7 +104,6 @@ constructor( @@ -109,7 +104,6 @@ constructor(
109 } 104 }
110 coroutineScope = CoroutineScope(defaultDispatcher + SupervisorJob()) 105 coroutineScope = CoroutineScope(defaultDispatcher + SupervisorJob())
111 state = State.CONNECTING 106 state = State.CONNECTING
112 - ::state.flow  
113 val response = engine.join(url, token, options) 107 val response = engine.join(url, token, options)
114 LKLog.i { "Connected to server, server version: ${response.serverVersion}, client version: ${Version.CLIENT_VERSION}" } 108 LKLog.i { "Connected to server, server version: ${response.serverVersion}, client version: ${Version.CLIENT_VERSION}" }
115 109
@@ -7,6 +7,7 @@ import io.livekit.android.room.track.LocalTrackPublication @@ -7,6 +7,7 @@ import io.livekit.android.room.track.LocalTrackPublication
7 import io.livekit.android.room.track.RemoteTrackPublication 7 import io.livekit.android.room.track.RemoteTrackPublication
8 import io.livekit.android.room.track.Track 8 import io.livekit.android.room.track.Track
9 import io.livekit.android.room.track.TrackPublication 9 import io.livekit.android.room.track.TrackPublication
  10 +import io.livekit.android.util.FlowObservable
10 import io.livekit.android.util.flow 11 import io.livekit.android.util.flow
11 import io.livekit.android.util.flowDelegate 12 import io.livekit.android.util.flowDelegate
12 import kotlinx.coroutines.CoroutineDispatcher 13 import kotlinx.coroutines.CoroutineDispatcher
@@ -32,23 +33,27 @@ open class Participant( @@ -32,23 +33,27 @@ open class Participant(
32 /** 33 /**
33 * Changes can be observed by using [io.livekit.android.util.flow] 34 * Changes can be observed by using [io.livekit.android.util.flow]
34 */ 35 */
  36 + @get:FlowObservable
35 var participantInfo: LivekitModels.ParticipantInfo? by flowDelegate(null) 37 var participantInfo: LivekitModels.ParticipantInfo? by flowDelegate(null)
36 private set 38 private set
37 39
38 /** 40 /**
39 * Changes can be observed by using [io.livekit.android.util.flow] 41 * Changes can be observed by using [io.livekit.android.util.flow]
40 */ 42 */
  43 + @get:FlowObservable
41 var identity: String? by flowDelegate(identity) 44 var identity: String? by flowDelegate(identity)
42 internal set 45 internal set
43 46
44 /** 47 /**
45 * Changes can be observed by using [io.livekit.android.util.flow] 48 * Changes can be observed by using [io.livekit.android.util.flow]
46 */ 49 */
  50 + @get:FlowObservable
47 var audioLevel: Float by flowDelegate(0f) 51 var audioLevel: Float by flowDelegate(0f)
48 internal set 52 internal set
49 /** 53 /**
50 * Changes can be observed by using [io.livekit.android.util.flow] 54 * Changes can be observed by using [io.livekit.android.util.flow]
51 */ 55 */
  56 + @get:FlowObservable
52 var isSpeaking: Boolean by flowDelegate(false) { newValue, oldValue -> 57 var isSpeaking: Boolean by flowDelegate(false) { newValue, oldValue ->
53 if (newValue != oldValue) { 58 if (newValue != oldValue) {
54 listener?.onSpeakingChanged(this) 59 listener?.onSpeakingChanged(this)
@@ -60,6 +65,7 @@ open class Participant( @@ -60,6 +65,7 @@ open class Participant(
60 /** 65 /**
61 * Changes can be observed by using [io.livekit.android.util.flow] 66 * Changes can be observed by using [io.livekit.android.util.flow]
62 */ 67 */
  68 + @get:FlowObservable
63 var metadata: String? by flowDelegate(null) { newMetadata, oldMetadata -> 69 var metadata: String? by flowDelegate(null) { newMetadata, oldMetadata ->
64 if (newMetadata != oldMetadata) { 70 if (newMetadata != oldMetadata) {
65 listener?.onMetadataChanged(this, oldMetadata) 71 listener?.onMetadataChanged(this, oldMetadata)
@@ -72,6 +78,7 @@ open class Participant( @@ -72,6 +78,7 @@ open class Participant(
72 /** 78 /**
73 * Changes can be observed by using [io.livekit.android.util.flow] 79 * Changes can be observed by using [io.livekit.android.util.flow]
74 */ 80 */
  81 + @get:FlowObservable
75 var connectionQuality by flowDelegate(ConnectionQuality.UNKNOWN) 82 var connectionQuality by flowDelegate(ConnectionQuality.UNKNOWN)
76 internal set 83 internal set
77 84
@@ -93,11 +100,13 @@ open class Participant( @@ -93,11 +100,13 @@ open class Participant(
93 /** 100 /**
94 * Changes can be observed by using [io.livekit.android.util.flow] 101 * Changes can be observed by using [io.livekit.android.util.flow]
95 */ 102 */
  103 + @get:FlowObservable
96 var tracks by flowDelegate(emptyMap<String, TrackPublication>()) 104 var tracks by flowDelegate(emptyMap<String, TrackPublication>())
97 protected set 105 protected set
98 /** 106 /**
99 * Changes can be observed by using [io.livekit.android.util.flow] 107 * Changes can be observed by using [io.livekit.android.util.flow]
100 */ 108 */
  109 + @get:FlowObservable
101 val audioTracks by flowDelegate( 110 val audioTracks by flowDelegate(
102 stateFlow = ::tracks.flow 111 stateFlow = ::tracks.flow
103 .map { it.filterValues { publication -> publication.kind == Track.Kind.AUDIO } } 112 .map { it.filterValues { publication -> publication.kind == Track.Kind.AUDIO } }
@@ -106,6 +115,7 @@ open class Participant( @@ -106,6 +115,7 @@ open class Participant(
106 /** 115 /**
107 * Changes can be observed by using [io.livekit.android.util.flow] 116 * Changes can be observed by using [io.livekit.android.util.flow]
108 */ 117 */
  118 + @get:FlowObservable
109 val videoTracks by flowDelegate( 119 val videoTracks by flowDelegate(
110 stateFlow = ::tracks.flow 120 stateFlow = ::tracks.flow
111 .map { 121 .map {
1 package io.livekit.android.room.track 1 package io.livekit.android.room.track
2 2
3 import io.livekit.android.room.participant.Participant 3 import io.livekit.android.room.participant.Participant
  4 +import io.livekit.android.util.FlowObservable
4 import io.livekit.android.util.flowDelegate 5 import io.livekit.android.util.flowDelegate
5 import livekit.LivekitModels 6 import livekit.LivekitModels
6 import java.lang.ref.WeakReference 7 import java.lang.ref.WeakReference
@@ -10,6 +11,7 @@ open class TrackPublication( @@ -10,6 +11,7 @@ open class TrackPublication(
10 track: Track?, 11 track: Track?,
11 participant: Participant 12 participant: Participant
12 ) { 13 ) {
  14 + @get:FlowObservable
13 open var track: Track? by flowDelegate(track) 15 open var track: Track? by flowDelegate(track)
14 internal set 16 internal set
15 var name: String 17 var name: String
@@ -62,7 +62,16 @@ internal val <T> KProperty0<T>.delegate: Any? @@ -62,7 +62,16 @@ internal val <T> KProperty0<T>.delegate: Any?
62 val <T> KProperty0<T>.flow: StateFlow<T> 62 val <T> KProperty0<T>.flow: StateFlow<T>
63 get() = delegate as StateFlow<T> 63 get() = delegate as StateFlow<T>
64 64
65 -class MutableStateFlowDelegate<T> 65 +/**
  66 + * Indicates that the target property changes can be observed with [flow].
  67 + */
  68 +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY_GETTER)
  69 +@Retention(AnnotationRetention.BINARY)
  70 +@MustBeDocumented
  71 +annotation class FlowObservable
  72 +
  73 +@FlowObservable
  74 +internal class MutableStateFlowDelegate<T>
66 internal constructor( 75 internal constructor(
67 private val flow: MutableStateFlow<T>, 76 private val flow: MutableStateFlow<T>,
68 private val onSetValue: ((newValue: T, oldValue: T) -> Unit)? = null 77 private val onSetValue: ((newValue: T, oldValue: T) -> Unit)? = null
@@ -82,7 +91,8 @@ internal constructor( @@ -82,7 +91,8 @@ internal constructor(
82 } 91 }
83 } 92 }
84 93
85 -class StateFlowDelegate<T> 94 +@FlowObservable
  95 +internal class StateFlowDelegate<T>
86 internal constructor( 96 internal constructor(
87 private val flow: StateFlow<T> 97 private val flow: StateFlow<T>
88 ) : StateFlow<T> by flow { 98 ) : StateFlow<T> by flow {
@@ -19,11 +19,14 @@ @@ -19,11 +19,14 @@
19 package io.livekit.lint 19 package io.livekit.lint
20 20
21 import com.android.tools.lint.detector.api.* 21 import com.android.tools.lint.detector.api.*
  22 +import com.intellij.psi.PsiClassType
22 import com.intellij.psi.PsiElement 23 import com.intellij.psi.PsiElement
  24 +import com.intellij.psi.PsiField
23 import com.intellij.psi.PsiMethod 25 import com.intellij.psi.PsiMethod
24 import org.jetbrains.uast.UCallableReferenceExpression 26 import org.jetbrains.uast.UCallableReferenceExpression
25 import org.jetbrains.uast.UReferenceExpression 27 import org.jetbrains.uast.UReferenceExpression
26 import org.jetbrains.uast.kotlin.KotlinUQualifiedReferenceExpression 28 import org.jetbrains.uast.kotlin.KotlinUQualifiedReferenceExpression
  29 +import org.jetbrains.uast.tryResolve
27 30
28 /** Checks related to DiffUtil computation. */ 31 /** Checks related to DiffUtil computation. */
29 class FlowDelegateUsageDetector : Detector(), SourceCodeScanner { 32 class FlowDelegateUsageDetector : Detector(), SourceCodeScanner {
@@ -32,20 +35,33 @@ class FlowDelegateUsageDetector : Detector(), SourceCodeScanner { @@ -32,20 +35,33 @@ class FlowDelegateUsageDetector : Detector(), SourceCodeScanner {
32 35
33 // Check if we're actually trying to access the flow delegate 36 // Check if we're actually trying to access the flow delegate
34 val referencedMethod = referenced as? PsiMethod ?: return 37 val referencedMethod = referenced as? PsiMethod ?: return
35 - if (referenced.name != "getFlow" || referencedMethod.containingClass?.qualifiedName != "io.livekit.android.util.FlowObservableKt") { 38 + if (referenced.name != GET_FLOW || referencedMethod.containingClass?.qualifiedName != FLOW_DELEGATE) {
36 return 39 return
37 } 40 }
38 41
39 - // This should get the property we're trying to receive the flow from. 42 + // This should get the getter we're trying to receive the flow from.
  43 + val parent = reference.uastParent as? KotlinUQualifiedReferenceExpression
  44 + val rec = parent?.receiver
  45 + val resolve = rec?.tryResolve()
  46 + println("parent: $parent, rec: $rec, resolve: $resolve")
40 val receiver = ((reference.uastParent as? KotlinUQualifiedReferenceExpression) 47 val receiver = ((reference.uastParent as? KotlinUQualifiedReferenceExpression)
41 ?.receiver as? UCallableReferenceExpression) 48 ?.receiver as? UCallableReferenceExpression)
42 - ?: return 49 + ?.resolve()
43 50
44 - // This should get the original class associated with the property.  
45 - val className = receiver.qualifierType?.canonicalText  
46 - val psiClass = if (className != null) context.evaluator.findClass(className) else null  
47 - val psiField = psiClass?.findFieldByName("${receiver.callableName}\$delegate", true)  
48 - val isAnnotated = psiField?.hasAnnotation("io.livekit.android.util.FlowObservable") ?: false 51 + val isAnnotated = when (receiver) {
  52 + is PsiMethod -> {
  53 + println("${receiver.name}, ${receiver.annotations.fold("") { total, next -> "$total, ${next.text}" }}")
  54 + receiver.hasAnnotation(FLOW_OBSERVABLE_ANNOTATION)
  55 + }
  56 + is PsiField -> {
  57 + val receiverClass = (receiver.type as? PsiClassType)?.resolve()
  58 + println("${receiverClass}, ${receiverClass?.annotations?.fold("") { total, next -> "$total, ${next.text}" }}")
  59 + receiverClass?.hasAnnotation(FLOW_OBSERVABLE_ANNOTATION) ?: false
  60 + }
  61 + else -> {
  62 + false
  63 + }
  64 + }
49 65
50 if (!isAnnotated) { 66 if (!isAnnotated) {
51 val message = DEFAULT_MSG 67 val message = DEFAULT_MSG
@@ -60,8 +76,16 @@ class FlowDelegateUsageDetector : Detector(), SourceCodeScanner { @@ -60,8 +76,16 @@ class FlowDelegateUsageDetector : Detector(), SourceCodeScanner {
60 76
61 companion object { 77 companion object {
62 78
  79 + // The name of the method for the flow accessor
  80 + private const val GET_FLOW = "getFlow"
  81 +
  82 + // The containing file and implicitly generated class
  83 + private const val FLOW_DELEGATE = "io.livekit.android.util.FlowDelegateKt"
  84 +
  85 + private const val FLOW_OBSERVABLE_ANNOTATION = "io.livekit.android.util.FlowObservable"
  86 +
63 private const val DEFAULT_MSG = 87 private const val DEFAULT_MSG =
64 - "Incorrect flow property usage: Only properties marked with the @FlowObservable annotation can be observed using `io.livekit.android.util.flow`." 88 + "Incorrect flow property usage: Only properties marked with the @FlowObservable annotation can be observed using `io.livekit.android.util.flow`. Improper usage will result in a NullPointerException."
65 89
66 private val IMPLEMENTATION = 90 private val IMPLEMENTATION =
67 Implementation(FlowDelegateUsageDetector::class.java, Scope.JAVA_FILE_SCOPE) 91 Implementation(FlowDelegateUsageDetector::class.java, Scope.JAVA_FILE_SCOPE)
1 package io.livekit.lint 1 package io.livekit.lint
2 2
3 import com.android.tools.lint.client.api.IssueRegistry 3 import com.android.tools.lint.client.api.IssueRegistry
  4 +import com.android.tools.lint.client.api.Vendor
4 import com.android.tools.lint.detector.api.CURRENT_API 5 import com.android.tools.lint.detector.api.CURRENT_API
5 import com.android.tools.lint.detector.api.Issue 6 import com.android.tools.lint.detector.api.Issue
6 import com.google.auto.service.AutoService 7 import com.google.auto.service.AutoService
@@ -11,6 +12,12 @@ class IssueRegistry : IssueRegistry() { @@ -11,6 +12,12 @@ class IssueRegistry : IssueRegistry() {
11 12
12 override val api: Int = CURRENT_API 13 override val api: Int = CURRENT_API
13 14
  15 + override val vendor: Vendor = Vendor(
  16 + vendorName = "LiveKit",
  17 + identifier = "io.livekit.android",
  18 + feedbackUrl = "https://github.com/livekit/client-sdk-android",
  19 + )
  20 +
14 override val issues: List<Issue> 21 override val issues: List<Issue>
15 - get() = listOf(MediaTrackEqualsDetector.ISSUE) 22 + get() = listOf(MediaTrackEqualsDetector.ISSUE, FlowDelegateUsageDetector.ISSUE)
16 } 23 }
1 @file:Suppress("UnstableApiUsage") // We know that Lint API's aren't final. 1 @file:Suppress("UnstableApiUsage") // We know that Lint API's aren't final.
2 -  
3 -/*  
4 - * Copyright (C) 2018 The Android Open Source Project  
5 - *  
6 - * Licensed under the Apache License, Version 2.0 (the "License");  
7 - * you may not use this file except in compliance with the License.  
8 - * You may obtain a copy of the License at  
9 - *  
10 - * http://www.apache.org/licenses/LICENSE-2.0  
11 - *  
12 - * Unless required by applicable law or agreed to in writing, software  
13 - * distributed under the License is distributed on an "AS IS" BASIS,  
14 - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  
15 - * See the License for the specific language governing permissions and  
16 - * limitations under the License.  
17 - */  
18 -  
19 package io.livekit.lint 2 package io.livekit.lint
20 3
21 import com.android.tools.lint.client.api.UElementHandler 4 import com.android.tools.lint.client.api.UElementHandler
@@ -14,6 +14,7 @@ class FlowDelegateUsageDetectorTest { @@ -14,6 +14,7 @@ class FlowDelegateUsageDetectorTest {
14 .allowMissingSdk() 14 .allowMissingSdk()
15 .files( 15 .files(
16 flowAccess(), 16 flowAccess(),
  17 + stateFlow(),
17 kotlin( 18 kotlin(
18 """ 19 """
19 package foo 20 package foo
@@ -21,7 +22,7 @@ class FlowDelegateUsageDetectorTest { @@ -21,7 +22,7 @@ class FlowDelegateUsageDetectorTest {
21 import io.livekit.android.util.flow 22 import io.livekit.android.util.flow
22 import io.livekit.android.util.flowDelegate 23 import io.livekit.android.util.flowDelegate
23 class Example { 24 class Example {
24 - @field:FlowObservable 25 + @get:FlowObservable
25 val value: Int by flowDelegate(0) 26 val value: Int by flowDelegate(0)
26 fun foo() { 27 fun foo() {
27 ::value.flow 28 ::value.flow
@@ -36,11 +37,12 @@ class FlowDelegateUsageDetectorTest { @@ -36,11 +37,12 @@ class FlowDelegateUsageDetectorTest {
36 } 37 }
37 38
38 @Test 39 @Test
39 - fun nonAnnotatedFlowAccess() { 40 + fun thisColonAccess() {
40 lint() 41 lint()
41 .allowMissingSdk() 42 .allowMissingSdk()
42 .files( 43 .files(
43 flowAccess(), 44 flowAccess(),
  45 + stateFlow(),
44 kotlin( 46 kotlin(
45 """ 47 """
46 package foo 48 package foo
@@ -48,6 +50,7 @@ class FlowDelegateUsageDetectorTest { @@ -48,6 +50,7 @@ class FlowDelegateUsageDetectorTest {
48 import io.livekit.android.util.flow 50 import io.livekit.android.util.flow
49 import io.livekit.android.util.flowDelegate 51 import io.livekit.android.util.flowDelegate
50 class Example { 52 class Example {
  53 + @get:FlowObservable
51 val value: Int by flowDelegate(0) 54 val value: Int by flowDelegate(0)
52 fun foo() { 55 fun foo() {
53 this::value.flow 56 this::value.flow
@@ -58,18 +61,143 @@ class FlowDelegateUsageDetectorTest { @@ -58,18 +61,143 @@ class FlowDelegateUsageDetectorTest {
58 ) 61 )
59 .issues(FlowDelegateUsageDetector.ISSUE) 62 .issues(FlowDelegateUsageDetector.ISSUE)
60 .run() 63 .run()
  64 + .expectClean()
  65 + }
  66 +
  67 + @Test
  68 + fun otherClassAccess() {
  69 + lint()
  70 + .allowMissingSdk()
  71 + .files(
  72 + flowAccess(),
  73 + stateFlow(),
  74 + kotlin(
  75 + """
  76 + package foo
  77 + import io.livekit.android.util.FlowObservable
  78 + import io.livekit.android.util.flow
  79 + import io.livekit.android.util.flowDelegate
  80 + class FlowContainer {
  81 + @get:FlowObservable
  82 + val value: Int by flowDelegate(0)
  83 + }
  84 + class Example {
  85 + fun foo() {
  86 + val container = FlowContainer()
  87 + container::value.flow
  88 + return
  89 + }
  90 + }"""
  91 + ).indented()
  92 + )
  93 + .issues(FlowDelegateUsageDetector.ISSUE)
  94 + .run()
  95 + .expectClean()
  96 + }
  97 +
  98 + @Test
  99 + fun parenthesesClassAccess() {
  100 + lint()
  101 + .allowMissingSdk()
  102 + .files(
  103 + flowAccess(),
  104 + stateFlow(),
  105 + kotlin(
  106 + """
  107 + package foo
  108 + import io.livekit.android.util.FlowObservable
  109 + import io.livekit.android.util.flow
  110 + import io.livekit.android.util.flowDelegate
  111 + class FlowContainer {
  112 + @get:FlowObservable
  113 + val value: Int by flowDelegate(0)
  114 + }
  115 + class Example {
  116 + fun foo() {
  117 + val container = FlowContainer()
  118 + (container)::value.flow
  119 + return
  120 + }
  121 + }"""
  122 + ).indented()
  123 + )
  124 + .issues(FlowDelegateUsageDetector.ISSUE)
  125 + .run()
  126 + .expectClean()
  127 + }
  128 +
  129 + @Test
  130 + fun roundaboutAccess() {
  131 + lint()
  132 + .allowMissingSdk()
  133 + .files(
  134 + flowAccess(),
  135 + stateFlow(),
  136 + kotlin(
  137 + """
  138 + package foo
  139 + import io.livekit.android.util.FlowObservable
  140 + import io.livekit.android.util.flow
  141 + import io.livekit.android.util.flowDelegate
  142 + class FlowContainer {
  143 + var value: Int by flowDelegate(0)
  144 + @get:FlowObservable
  145 + val otherValue: Int
  146 + get() = value
  147 + }
  148 + class Example {
  149 + fun foo() {
  150 + val container = FlowContainer()
  151 + container::otherValue.flow
  152 + return
  153 + }
  154 + }"""
  155 + ).indented()
  156 + )
  157 + .issues(FlowDelegateUsageDetector.ISSUE)
  158 + .run()
  159 + .expectClean()
  160 + }
  161 +
  162 + @Test
  163 + fun nonAnnotatedFlowAccess() {
  164 + lint()
  165 + .allowMissingSdk()
  166 + .files(
  167 + flowAccess(),
  168 + stateFlow(),
  169 + kotlin(
  170 + """
  171 + package foo
  172 + import io.livekit.android.util.FlowObservable
  173 + import io.livekit.android.util.flow
  174 + import io.livekit.android.util.flowDelegate
  175 + class Example {
  176 + val value: Int = 0
  177 + fun foo() {
  178 + this::value.flow
  179 + return
  180 + }
  181 + }"""
  182 + ).indented()
  183 + )
  184 + .issues(FlowDelegateUsageDetector.ISSUE)
  185 + .run()
61 .expectErrorCount(1) 186 .expectErrorCount(1)
62 } 187 }
63 } 188 }
64 189
65 fun flowAccess(): TestFile { 190 fun flowAccess(): TestFile {
66 return kotlin( 191 return kotlin(
  192 + "io/livekit/android/util/FlowDelegate.kt",
67 """ 193 """
68 package io.livekit.android.util 194 package io.livekit.android.util
69 195
70 import kotlin.reflect.KProperty 196 import kotlin.reflect.KProperty
71 import kotlin.reflect.KProperty0 197 import kotlin.reflect.KProperty0
72 - 198 + import kotlinx.coroutines.flow.StateFlow
  199 + import kotlinx.coroutines.flow.MutableStateFlow
  200 +
73 internal val <T> KProperty0<T>.delegate: Any? 201 internal val <T> KProperty0<T>.delegate: Any?
74 get() { getDelegate() } 202 get() { getDelegate() }
75 203
@@ -77,11 +205,12 @@ fun flowAccess(): TestFile { @@ -77,11 +205,12 @@ fun flowAccess(): TestFile {
77 val <T> KProperty0<T>.flow: StateFlow<T> 205 val <T> KProperty0<T>.flow: StateFlow<T>
78 get() = delegate as StateFlow<T> 206 get() = delegate as StateFlow<T>
79 207
80 - @Target(AnnotationTarget.PROPERTY) 208 + @Target(AnnotationTarget.PROPERTY_GETTER)
81 @Retention(AnnotationRetention.SOURCE) 209 @Retention(AnnotationRetention.SOURCE)
82 @MustBeDocumented 210 @MustBeDocumented
83 annotation class FlowObservable 211 annotation class FlowObservable
84 212
  213 + @FlowObservable
85 class MutableStateFlowDelegate<T> 214 class MutableStateFlowDelegate<T>
86 internal constructor( 215 internal constructor(
87 private val flow: MutableStateFlow<T>, 216 private val flow: MutableStateFlow<T>,
@@ -105,7 +234,16 @@ fun flowAccess(): TestFile { @@ -105,7 +234,16 @@ fun flowAccess(): TestFile {
105 ): MutableStateFlowDelegate<T> { 234 ): MutableStateFlowDelegate<T> {
106 return MutableStateFlowDelegate(MutableStateFlow(initialValue), onSetValue) 235 return MutableStateFlowDelegate(MutableStateFlow(initialValue), onSetValue)
107 } 236 }
  237 + """
  238 + ).indented()
  239 + .within("src")
  240 +}
108 241
  242 +fun stateFlow(): TestFile {
  243 +
  244 + return kotlin(
  245 + """
  246 + package kotlinx.coroutines.flow
109 interface StateFlow<out T> { 247 interface StateFlow<out T> {
110 val value: T 248 val value: T
111 } 249 }