David Liu

FlowDelegateUsageDetector

@@ -20,194 +20,332 @@ package io.livekit.lint @@ -20,194 +20,332 @@ package io.livekit.lint
20 20
21 import com.android.tools.lint.client.api.UElementHandler 21 import com.android.tools.lint.client.api.UElementHandler
22 import com.android.tools.lint.detector.api.* 22 import com.android.tools.lint.detector.api.*
23 -import com.intellij.psi.CommonClassNames.JAVA_LANG_OBJECT  
24 -import com.intellij.psi.PsiClassType 23 +import com.intellij.psi.JavaElementVisitor
25 import com.intellij.psi.PsiElement 24 import com.intellij.psi.PsiElement
  25 +import com.intellij.psi.PsiJavaCodeReferenceElement
26 import com.intellij.psi.PsiMethod 26 import com.intellij.psi.PsiMethod
27 import org.jetbrains.uast.* 27 import org.jetbrains.uast.*
28 -import org.jetbrains.uast.util.isMethodCall 28 +import org.jetbrains.uast.kotlin.KotlinUQualifiedReferenceExpression
29 29
30 /** Checks related to DiffUtil computation. */ 30 /** Checks related to DiffUtil computation. */
31 class FlowDelegateUsageDetector : Detector(), SourceCodeScanner { 31 class FlowDelegateUsageDetector : Detector(), SourceCodeScanner {
32 32
  33 + override fun visitReference(
  34 + context: JavaContext,
  35 + visitor: JavaElementVisitor?,
  36 + reference: PsiJavaCodeReferenceElement,
  37 + referenced: PsiElement
  38 + ) {
  39 + super.visitReference(context, visitor, reference, referenced)
  40 + }
  41 +
  42 + override fun visitReference(context: JavaContext, reference: UReferenceExpression, referenced: PsiElement) {
  43 +
  44 + // Check if we're actually trying to access the flow delegate
  45 + val referencedMethod = referenced as? PsiMethod ?: return
  46 + if (referenced.name != "getFlow" || referencedMethod.containingClass?.qualifiedName != "io.livekit.android.util.FlowObservableKt") {
  47 + return
  48 + }
  49 +
  50 + // This should get the property we're trying to receive the flow from.
  51 + val receiver = ((reference.uastParent as? KotlinUQualifiedReferenceExpression)
  52 + ?.receiver as? UCallableReferenceExpression)
  53 + ?: return
  54 +
  55 + // This should get the original class associated with the property.
  56 + val className = receiver.qualifierType?.canonicalText
  57 + val psiClass = if (className != null) context.evaluator.findClass(className) else null
  58 + val psiField = psiClass?.findFieldByName("${receiver.callableName}\$delegate", true)
  59 + val isAnnotated = psiField?.hasAnnotation("io.livekit.android.util.FlowObservable") ?: false
  60 +
  61 + if (!isAnnotated) {
  62 + val message = DEFAULT_MSG
  63 + val location = context.getLocation(reference)
  64 + context.report(ISSUE, reference, location, message)
  65 + }
  66 + }
  67 +
  68 + override fun getApplicableReferenceNames(): List<String>? =
  69 + listOf("flow")
  70 +
  71 + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
  72 + super.visitMethodCall(context, node, method)
  73 + }
  74 +
33 override fun getApplicableUastTypes() = 75 override fun getApplicableUastTypes() =
34 - listOf(UBinaryExpression::class.java, UCallExpression::class.java) 76 + listOf(
  77 + UBinaryExpression::class.java,
  78 + UCallExpression::class.java,
  79 + UAnnotation::class.java,
  80 + UArrayAccessExpression::class.java,
  81 + UBinaryExpressionWithType::class.java,
  82 + UBlockExpression::class.java,
  83 + UBreakExpression::class.java,
  84 + UCallableReferenceExpression::class.java,
  85 + UCatchClause::class.java,
  86 + UClass::class.java,
  87 + UClassLiteralExpression::class.java,
  88 + UContinueExpression::class.java,
  89 + UDeclaration::class.java,
  90 + UDoWhileExpression::class.java,
  91 + UElement::class.java,
  92 + UEnumConstant::class.java,
  93 + UExpression::class.java,
  94 + UExpressionList::class.java,
  95 + UField::class.java,
  96 + UFile::class.java,
  97 + UForEachExpression::class.java,
  98 + UForExpression::class.java,
  99 + UIfExpression::class.java,
  100 + UImportStatement::class.java,
  101 + UClassInitializer::class.java,
  102 + ULabeledExpression::class.java,
  103 + ULambdaExpression::class.java,
  104 + ULiteralExpression::class.java,
  105 + ULocalVariable::class.java,
  106 + UMethod::class.java,
  107 + UObjectLiteralExpression::class.java,
  108 + UParameter::class.java,
  109 + UParenthesizedExpression::class.java,
  110 + UPolyadicExpression::class.java,
  111 + UPostfixExpression::class.java,
  112 + UPrefixExpression::class.java,
  113 + UQualifiedReferenceExpression::class.java,
  114 + UReturnExpression::class.java,
  115 + USimpleNameReferenceExpression::class.java,
  116 + USuperExpression::class.java,
  117 + USwitchClauseExpression::class.java,
  118 + USwitchExpression::class.java,
  119 + UThisExpression::class.java,
  120 + UThrowExpression::class.java,
  121 + UTryExpression::class.java,
  122 + UTypeReferenceExpression::class.java,
  123 + UUnaryExpression::class.java,
  124 + UVariable::class.java,
  125 + UWhileExpression::class.java,
  126 + UYieldExpression::class.java,
  127 +
  128 + )
35 129
36 override fun createUastHandler(context: JavaContext): UElementHandler? { 130 override fun createUastHandler(context: JavaContext): UElementHandler? {
37 return object : UElementHandler() { 131 return object : UElementHandler() {
38 - 132 + val context = context
39 override fun visitBinaryExpression(node: UBinaryExpression) { 133 override fun visitBinaryExpression(node: UBinaryExpression) {
40 - checkExpression(context, node) 134 + println(0)
41 } 135 }
42 136
43 override fun visitCallExpression(node: UCallExpression) { 137 override fun visitCallExpression(node: UCallExpression) {
44 - checkCall(context, node) 138 + node.classReference
  139 + node.methodName
  140 + node.methodIdentifier
  141 + node.receiverType
  142 + node.receiver
  143 + node.kind
  144 + node.valueArguments
  145 + node.valueArgumentCount
  146 + node.typeArguments
  147 + node.typeArgumentCount
  148 + node.returnType
  149 + println(1)
45 } 150 }
46 - }  
47 - }  
48 151
49 - private fun defaultEquals(context: JavaContext, node: UElement): Boolean {  
50 - val resolved: PsiMethod? 152 + override fun visitAnnotation(node: UAnnotation) {
  153 + println(0)
  154 + }
51 155
52 - when (node) {  
53 - is UBinaryExpression -> {  
54 - resolved = node.resolveOperator()  
55 - if (resolved == null) {  
56 - val left = node.leftOperand.getExpressionType() as? PsiClassType  
57 - return defaultEquals(context, left)  
58 - } 156 + override fun visitArrayAccessExpression(node: UArrayAccessExpression) {
  157 + println(0)
59 } 158 }
60 - is UCallExpression -> {  
61 - resolved = node.takeIf { it.isMethodCall() }  
62 - ?.resolve() 159 +
  160 + override fun visitBinaryExpressionWithType(node: UBinaryExpressionWithType) {
  161 + println(0)
63 } 162 }
64 - is UParenthesizedExpression -> {  
65 - return defaultEquals(context, node.expression) 163 +
  164 + override fun visitBlockExpression(node: UBlockExpression) {
  165 + println(0)
66 } 166 }
67 - else -> {  
68 - // We don't know any better  
69 - return false 167 +
  168 + override fun visitBreakExpression(node: UBreakExpression) {
  169 + println(0)
70 } 170 }
71 - }  
72 171
73 - return resolved?.containingClass?.qualifiedName == MEDIA_STREAM_TRACK  
74 - } 172 + override fun visitCallableReferenceExpression(node: UCallableReferenceExpression) {
  173 + println(0)
  174 + }
75 175
76 - private fun defaultEquals(  
77 - context: JavaContext,  
78 - type: PsiClassType?  
79 - ): Boolean {  
80 - val cls = type?.resolve() ?: return false  
81 -  
82 - if (isKotlin(cls) && (context.evaluator.isSealed(cls) || context.evaluator.isData(cls))) {  
83 - // Sealed class doesn't guarantee that it defines equals/hashCode  
84 - // but it's likely (we'd need to go look at each inner class)  
85 - return false  
86 - } 176 + override fun visitCatchClause(node: UCatchClause) {
  177 + println(0)
  178 + }
87 179
88 - for (m in cls.findMethodsByName("equals", true)) {  
89 - if (m is PsiMethod) {  
90 - val parameters = m.parameterList.parameters  
91 - if (parameters.size == 1 &&  
92 - parameters[0].type.canonicalText == MEDIA_STREAM_TRACK  
93 - ) {  
94 - return m.containingClass?.qualifiedName == MEDIA_STREAM_TRACK  
95 - } 180 + override fun visitClass(node: UClass) {
  181 + println(0)
96 } 182 }
97 - }  
98 183
99 - return false  
100 - } 184 + override fun visitClassLiteralExpression(node: UClassLiteralExpression) {
  185 + println(0)
  186 + }
101 187
102 - private fun checkCall(context: JavaContext, node: UCallExpression) {  
103 - if (defaultEquals(context, node)) {  
104 - // Within cast or instanceof check which implies a more specific type  
105 - // which provides an equals implementation?  
106 - if (withinCastWithEquals(context, node)) {  
107 - return 188 + override fun visitContinueExpression(node: UContinueExpression) {
  189 + println(0)
108 } 190 }
109 191
110 - val message = DEFAULT_MSG  
111 - val location = context.getCallLocation(  
112 - node,  
113 - includeReceiver = false,  
114 - includeArguments = true  
115 - )  
116 - context.report(ISSUE, node, location, message)  
117 - }  
118 - } 192 + override fun visitDeclaration(node: UDeclaration) {
  193 + println(0)
  194 + }
119 195
120 - /**  
121 - * Is this .equals() call within another if check which checks  
122 - * instanceof on a more specific type than we're calling equals on?  
123 - * If so, does that more specific type define its own equals?  
124 - *  
125 - * Also handle an implicit check via short circuit evaluation; e.g.  
126 - * something like "return a is A && b is B && a.equals(b)".  
127 - */  
128 - private fun withinCastWithEquals(context: JavaContext, node: UExpression): Boolean {  
129 - var parent = skipParenthesizedExprUp(node.uastParent)  
130 - if (parent is UQualifiedReferenceExpression) {  
131 - parent = skipParenthesizedExprUp(parent.uastParent)  
132 - }  
133 - val target: PsiElement? = when (node) {  
134 - is UCallExpression -> node.receiver?.tryResolve()  
135 - is UBinaryExpression -> node.leftOperand.tryResolve()  
136 - else -> null  
137 - } 196 + override fun visitDeclarationsExpression(node: UDeclarationsExpression) {
  197 + println(0)
  198 + }
138 199
139 - if (parent is UPolyadicExpression && parent.operator == UastBinaryOperator.LOGICAL_AND) {  
140 - val operands = parent.operands  
141 - for (operand in operands) {  
142 - if (operand === node) {  
143 - break  
144 - }  
145 - if (isCastWithEquals(context, operand, target)) {  
146 - return true  
147 - } 200 + override fun visitDoWhileExpression(node: UDoWhileExpression) {
  201 + println(0)
148 } 202 }
149 - }  
150 - val ifStatement = node.getParentOfType<UElement>(UIfExpression::class.java, false, UMethod::class.java)  
151 - as? UIfExpression ?: return false  
152 - val condition = ifStatement.condition  
153 - return isCastWithEquals(context, condition, target)  
154 - }  
155 203
156 - private fun isCastWithEquals(context: JavaContext, node: UExpression, target: PsiElement?): Boolean {  
157 - when {  
158 - node is UBinaryExpressionWithType -> {  
159 - if (target != null) {  
160 - val resolved = node.operand.tryResolve()  
161 - // Unfortunately in some scenarios isEquivalentTo returns false for equal instances  
162 - //noinspection LintImplPsiEquals  
163 - if (resolved != null && !(target == resolved || target.isEquivalentTo(resolved))) {  
164 - return false  
165 - }  
166 - }  
167 - return !defaultEquals(context, node.type as? PsiClassType)  
168 - }  
169 - node is UPolyadicExpression && node.operator == UastBinaryOperator.LOGICAL_AND -> {  
170 - for (operand in node.operands) {  
171 - if (isCastWithEquals(context, operand, target)) {  
172 - return true  
173 - }  
174 - }  
175 - }  
176 - node is UParenthesizedExpression -> {  
177 - return isCastWithEquals(context, node.expression, target) 204 + override fun visitElement(node: UElement) {
  205 + println(0)
  206 + }
  207 +
  208 + override fun visitEnumConstant(node: UEnumConstant) {
  209 + println(0)
  210 + }
  211 +
  212 + override fun visitExpression(node: UExpression) {
  213 + println(0)
  214 + }
  215 +
  216 + override fun visitExpressionList(node: UExpressionList) {
  217 + println(0)
  218 + }
  219 +
  220 + override fun visitField(node: UField) {
  221 + println(0)
  222 + }
  223 +
  224 + override fun visitFile(node: UFile) {
  225 + println(0)
  226 + }
  227 +
  228 + override fun visitForEachExpression(node: UForEachExpression) {
  229 + println(0)
  230 + }
  231 +
  232 + override fun visitForExpression(node: UForExpression) {
  233 + println(0)
  234 + }
  235 +
  236 + override fun visitIfExpression(node: UIfExpression) {
  237 + println(0)
  238 + }
  239 +
  240 + override fun visitImportStatement(node: UImportStatement) {
  241 + println(0)
  242 + }
  243 +
  244 + override fun visitInitializer(node: UClassInitializer) {
  245 + println(0)
  246 + }
  247 +
  248 + override fun visitLabeledExpression(node: ULabeledExpression) {
  249 + println(0)
  250 + }
  251 +
  252 + override fun visitLambdaExpression(node: ULambdaExpression) {
  253 + println(0)
  254 + }
  255 +
  256 + override fun visitLiteralExpression(node: ULiteralExpression) {
  257 + println(0)
  258 + }
  259 +
  260 + override fun visitLocalVariable(node: ULocalVariable) {
  261 + println(0)
  262 + }
  263 +
  264 + override fun visitMethod(node: UMethod) {
  265 + println(0)
  266 + }
  267 +
  268 +
  269 + override fun visitObjectLiteralExpression(node: UObjectLiteralExpression) {
  270 + println(0)
  271 + }
  272 +
  273 + override fun visitParameter(node: UParameter) {
  274 + println(0)
  275 + }
  276 +
  277 + override fun visitParenthesizedExpression(node: UParenthesizedExpression) {
  278 + println(0)
  279 + }
  280 +
  281 + override fun visitPolyadicExpression(node: UPolyadicExpression) {
  282 + println(0)
  283 + }
  284 +
  285 + override fun visitPostfixExpression(node: UPostfixExpression) {
  286 + println(0)
  287 + }
  288 +
  289 + override fun visitPrefixExpression(node: UPrefixExpression) {
  290 + println(0)
  291 + }
  292 +
  293 + override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) {
  294 + println(0)
  295 + }
  296 +
  297 + override fun visitReturnExpression(node: UReturnExpression) {
  298 + println(0)
  299 + }
  300 +
  301 + override fun visitSimpleNameReferenceExpression(node: USimpleNameReferenceExpression) {
  302 + println(0)
  303 +
  304 + //(((node as KotlinUSimpleReferenceExpression).uastParent as KotlinUQualifiedReferenceExpression).receiver as KotlinUCallableReferenceExpression).qualifierType?.canonicalText
  305 + }
  306 +
  307 + override fun visitSuperExpression(node: USuperExpression) {
  308 + println(0)
  309 + }
  310 +
  311 + override fun visitSwitchClauseExpression(node: USwitchClauseExpression) {
  312 + println(0)
  313 + }
  314 +
  315 + override fun visitSwitchExpression(node: USwitchExpression) {
  316 + println(0)
  317 + }
  318 +
  319 + override fun visitThisExpression(node: UThisExpression) {
  320 + println(0)
  321 + }
  322 +
  323 + override fun visitThrowExpression(node: UThrowExpression) {
  324 + println(0)
  325 + }
  326 +
  327 + override fun visitTryExpression(node: UTryExpression) {
  328 + println(0)
  329 + }
  330 +
  331 + override fun visitTypeReferenceExpression(node: UTypeReferenceExpression) {
  332 + println(0)
  333 + }
  334 +
  335 + override fun visitUnaryExpression(node: UUnaryExpression) {
  336 + println(0)
  337 + }
  338 +
  339 + override fun visitVariable(node: UVariable) {
  340 + println(0)
  341 + }
  342 +
  343 + override fun visitWhileExpression(node: UWhileExpression) {
  344 + println(0)
178 } 345 }
179 - }  
180 - return false  
181 - }  
182 346
183 - private fun checkExpression(context: JavaContext, node: UBinaryExpression) {  
184 - if (node.operator == UastBinaryOperator.IDENTITY_EQUALS ||  
185 - node.operator == UastBinaryOperator.EQUALS  
186 - ) {  
187 - val left = node.leftOperand.getExpressionType() ?: return  
188 - val right = node.rightOperand.getExpressionType() ?: return  
189 - if (left is PsiClassType && right is PsiClassType  
190 - && (left.className == "MediaStreamTrack" || right.className == "MediaStreamTrack")  
191 - ) {  
192 - if (node.operator == UastBinaryOperator.EQUALS) {  
193 - if (defaultEquals(context, node)) {  
194 - if (withinCastWithEquals(context, node)) {  
195 - return  
196 - }  
197 -  
198 - val message = DEFAULT_MSG  
199 - val location = node.operatorIdentifier?.let {  
200 - context.getLocation(it)  
201 - } ?: context.getLocation(node)  
202 - context.report(ISSUE, node, location, message)  
203 - }  
204 - } else {  
205 - val message = DEFAULT_MSG  
206 - val location = node.operatorIdentifier?.let {  
207 - context.getLocation(it)  
208 - } ?: context.getLocation(node)  
209 - context.report(ISSUE, node, location, message)  
210 - } 347 + override fun visitYieldExpression(node: UYieldExpression) {
  348 + println(0)
211 } 349 }
212 } 350 }
213 } 351 }
@@ -216,20 +354,18 @@ class FlowDelegateUsageDetector : Detector(), SourceCodeScanner { @@ -216,20 +354,18 @@ class FlowDelegateUsageDetector : Detector(), SourceCodeScanner {
216 private const val MEDIA_STREAM_TRACK = "org.webrtc.MediaStreamTrack" 354 private const val MEDIA_STREAM_TRACK = "org.webrtc.MediaStreamTrack"
217 355
218 private const val DEFAULT_MSG = 356 private const val DEFAULT_MSG =
219 - "Suspicious equality check: MediaStreamTracks should not be checked for equality. Check id() instead." 357 + "Incorrect flow property usage: Only properties marked with the @FlowObservable annotation can be observed using `io.livekit.android.util.flow`."
220 358
221 private val IMPLEMENTATION = 359 private val IMPLEMENTATION =
222 Implementation(FlowDelegateUsageDetector::class.java, Scope.JAVA_FILE_SCOPE) 360 Implementation(FlowDelegateUsageDetector::class.java, Scope.JAVA_FILE_SCOPE)
223 361
224 @JvmField 362 @JvmField
225 val ISSUE = Issue.create( 363 val ISSUE = Issue.create(
226 - id = "MediaTrackEqualsDetector",  
227 - briefDescription = "Suspicious DiffUtil Equality", 364 + id = "FlowDelegateUsageDetector",
  365 + briefDescription = "flow on a non-@FlowObservable property",
228 explanation = """ 366 explanation = """
229 - `areContentsTheSame` is used by `DiffUtil` to produce diffs. If the \  
230 - method is implemented incorrectly, such as using identity equals \  
231 - instead of equals, or calling equals on a class that has not implemented \  
232 - it, weird visual artifacts can occur. 367 + Only properties marked with the @FlowObservable annotation can be observed using
  368 + `io.livekit.android.util.flow`.
233 """, 369 """,
234 category = Category.CORRECTNESS, 370 category = Category.CORRECTNESS,
235 priority = 4, 371 priority = 4,
@@ -2,135 +2,114 @@ @@ -2,135 +2,114 @@
2 2
3 package io.livekit.lint 3 package io.livekit.lint
4 4
5 -import com.android.tools.lint.checks.infrastructure.LintDetectorTest.bytes  
6 import com.android.tools.lint.checks.infrastructure.TestFile 5 import com.android.tools.lint.checks.infrastructure.TestFile
7 -import com.android.tools.lint.checks.infrastructure.TestFiles.java  
8 -import com.android.tools.lint.checks.infrastructure.TestLintTask 6 +import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
9 import com.android.tools.lint.checks.infrastructure.TestLintTask.lint 7 import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
10 import org.junit.Test 8 import org.junit.Test
11 9
12 class FlowDelegateUsageDetectorTest { 10 class FlowDelegateUsageDetectorTest {
13 @Test 11 @Test
14 - fun objectEquals() { 12 + fun normalFlowAccess() {
15 lint() 13 lint()
16 .allowMissingSdk() 14 .allowMissingSdk()
17 .files( 15 .files(
18 - java( 16 + flowAccess(),
  17 + kotlin(
19 """ 18 """
20 - package foo;  
21 -  
22 - class Example {  
23 - public boolean foo() {  
24 - Object a = new Object();  
25 - Object b = new Object();  
26 - return a.equals(b);  
27 - }  
28 - }""" 19 + package foo
  20 + import io.livekit.android.util.FlowObservable
  21 + import io.livekit.android.util.flow
  22 + import io.livekit.android.util.flowDelegate
  23 + class Example {
  24 + @field:FlowObservable
  25 + val value: Int by flowDelegate(0)
  26 + fun foo() {
  27 + ::value.flow
  28 + return
  29 + }
  30 + }"""
29 ).indented() 31 ).indented()
30 ) 32 )
31 - .issues(MediaTrackEqualsDetector.ISSUE) 33 + .issues(FlowDelegateUsageDetector.ISSUE)
32 .run() 34 .run()
33 .expectClean() 35 .expectClean()
34 } 36 }
35 37
36 @Test 38 @Test
37 - fun objectEqualityOperator() { 39 + fun nonAnnotatedFlowAccess() {
38 lint() 40 lint()
39 .allowMissingSdk() 41 .allowMissingSdk()
40 .files( 42 .files(
41 - java( 43 + flowAccess(),
  44 + kotlin(
42 """ 45 """
43 - package foo;  
44 -  
45 - class Example {  
46 - public boolean foo() {  
47 - Object a = new Object();  
48 - Object b = new Object();  
49 - return a == b;  
50 - }  
51 - }""" 46 + package foo
  47 + import io.livekit.android.util.FlowObservable
  48 + import io.livekit.android.util.flow
  49 + import io.livekit.android.util.flowDelegate
  50 + class Example {
  51 + val value: Int by flowDelegate(0)
  52 + fun foo() {
  53 + this::value.flow
  54 + return
  55 + }
  56 + }"""
52 ).indented() 57 ).indented()
53 ) 58 )
54 - .issues(MediaTrackEqualsDetector.ISSUE)  
55 - .run()  
56 - .expectClean()  
57 - }  
58 -  
59 - @Test  
60 - fun mediaTrackEquals() {  
61 - lint()  
62 - .allowMissingSdk()  
63 - .files(  
64 - mediaStreamTrack(),  
65 - java(  
66 - """  
67 - package foo;  
68 - import org.webrtc.MediaStreamTrack;  
69 -  
70 - class Example {  
71 - public boolean foo() {  
72 - MediaStreamTrack a = new MediaStreamTrack();  
73 - MediaStreamTrack b = new MediaStreamTrack();  
74 - return a.equals(b);  
75 - }  
76 - }"""  
77 - ).indented()  
78 - )  
79 - .issues(MediaTrackEqualsDetector.ISSUE) 59 + .issues(FlowDelegateUsageDetector.ISSUE)
80 .run() 60 .run()
81 .expectErrorCount(1) 61 .expectErrorCount(1)
82 } 62 }
  63 +}
83 64
84 - @Test  
85 - fun mediaTrackEqualityOperator() {  
86 - lint()  
87 - .allowMissingSdk()  
88 - .files(  
89 - mediaStreamTrack(),  
90 - java(  
91 - """  
92 - package foo;  
93 - import org.webrtc.MediaStreamTrack;  
94 -  
95 - class Example {  
96 - public boolean foo() {  
97 - ABC a = new ABC();  
98 - MediaStreamTrack b = new MediaStreamTrack();  
99 - a.equals(b);  
100 - return a == b; 65 +fun flowAccess(): TestFile {
  66 + return kotlin(
  67 + """
  68 + package io.livekit.android.util
  69 +
  70 + import kotlin.reflect.KProperty
  71 + import kotlin.reflect.KProperty0
  72 +
  73 + internal val <T> KProperty0<T>.delegate: Any?
  74 + get() { getDelegate() }
  75 +
  76 + @Suppress("UNCHECKED_CAST")
  77 + val <T> KProperty0<T>.flow: StateFlow<T>
  78 + get() = delegate as StateFlow<T>
  79 +
  80 + @Target(AnnotationTarget.PROPERTY)
  81 + @Retention(AnnotationRetention.SOURCE)
  82 + @MustBeDocumented
  83 + annotation class FlowObservable
  84 +
  85 + class MutableStateFlowDelegate<T>
  86 + internal constructor(
  87 + private val flow: MutableStateFlow<T>,
  88 + private val onSetValue: ((newValue: T, oldValue: T) -> Unit)? = null
  89 + ) : MutableStateFlow<T> by flow {
  90 +
  91 + operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
  92 + return flow.value
101 } 93 }
102 - public boolean equals(Object o){  
103 - return false; 94 +
  95 + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
  96 + val oldValue = flow.value
  97 + flow.value = value
  98 + onSetValue?.invoke(value, oldValue)
104 } 99 }
105 - }"""  
106 - ).indented()  
107 - )  
108 - .issues(MediaTrackEqualsDetector.ISSUE)  
109 - .run()  
110 - .expectErrorCount(1)  
111 - }  
112 -  
113 - @Test  
114 - fun properMediaTrackEquality() {  
115 - lint()  
116 - .allowMissingSdk()  
117 - .files(  
118 - mediaStreamTrack(),  
119 - java(  
120 - """  
121 - package foo; 100 + }
  101 +
  102 + public fun <T> flowDelegate(
  103 + initialValue: T,
  104 + onSetValue: ((newValue: T, oldValue: T) -> Unit)? = null
  105 + ): MutableStateFlowDelegate<T> {
  106 + return MutableStateFlowDelegate(MutableStateFlow(initialValue), onSetValue)
  107 + }
122 108
123 - class Example {  
124 - public boolean foo() {  
125 - MediaStreamTrack a = new MediaStreamTrack();  
126 - MediaStreamTrack b = new MediaStreamTrack();  
127 - return a.getId() == b.getId();  
128 - }  
129 - }"""  
130 - ).indented()  
131 - )  
132 - .issues(MediaTrackEqualsDetector.ISSUE)  
133 - .run()  
134 - .expectClean()  
135 - } 109 + interface StateFlow<out T> {
  110 + val value: T
  111 + }
  112 + class MutableStateFlow<T>(override var value: T) : StateFlow<T>
  113 + """
  114 + ).indented()
136 } 115 }