David Liu

Merge branch 'flowlint'

<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
<bytecodeTargetLevel target="1.8">
<module name="livekit-android.livekit-android-sdk" target="11" />
<module name="livekit-android.sample-app" target="11" />
<module name="livekit-android.sample-app-common" target="11" />
<module name="livekit-android.sample-app-compose" target="11" />
</bytecodeTargetLevel>
</component>
</project>
\ No newline at end of file
... ...
... ... @@ -53,16 +53,34 @@ ext {
versions = [
androidx_core : "1.6.0",
androidx_lifecycle: "2.4.0",
autoService : '1.0.1',
dagger : "2.27",
groupie : "2.9.0",
junit : "4.13.2",
junitJupiter : "5.5.0",
lint : "30.0.1",
protobuf : "3.15.1",
]
generated = [
protoSrc: "$projectDir/protocol",
]
deps = [
auto : [
'service' : "com.google.auto.service:auto-service:${versions.autoService}",
'serviceAnnotations': "com.google.auto.service:auto-service-annotations:${versions.autoService}",
],
kotlinx_coroutines: "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2",
timber : "com.github.ajalt:timberkt:1.5.1",
timber : "com.github.ajalt:timberkt:1.5.1",
// lint
lint : "com.android.tools.lint:lint:${versions.lint}",
lintApi : "com.android.tools.lint:lint-api:${versions.lint}",
lintChecks : "com.android.tools.lint:lint-checks:${versions.lint}",
lintTests : "com.android.tools.lint:lint-tests:${versions.lint}",
// tests
junit : "junit:junit:${versions.junit}",
junitJupiterApi : "org.junit.jupiter:junit-jupiter-api:${versions.junitJupiter}",
junitJupiterEngine: "org.junit.jupiter:junit-jupiter-engine:${versions.junitJupiter}",
]
annotations = [
]
... ...
... ... @@ -118,6 +118,9 @@ dependencies {
implementation deps.timber
implementation 'com.vdurmont:semver4j:3.1.0'
lintChecks project(':livekit-lint')
lintPublish project(':livekit-lint')
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.6'
testImplementation 'org.mockito:mockito-core:4.0.0'
... ...
... ... @@ -16,6 +16,7 @@ import io.livekit.android.events.RoomEvent
import io.livekit.android.renderer.TextureViewRenderer
import io.livekit.android.room.participant.*
import io.livekit.android.room.track.*
import io.livekit.android.util.FlowObservable
import io.livekit.android.util.LKLog
import io.livekit.android.util.flow
import io.livekit.android.util.flowDelegate
... ... @@ -60,23 +61,18 @@ constructor(
@Deprecated("Use events instead.")
var listener: RoomListener? = null
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@get:FlowObservable
var sid: Sid? by flowDelegate(null)
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@get:FlowObservable
var name: String? by flowDelegate(null)
private set
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@get:FlowObservable
var state: State by flowDelegate(State.DISCONNECTED)
private set
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@get:FlowObservable
var metadata: String? by flowDelegate(null)
private set
... ... @@ -112,17 +108,16 @@ constructor(
lateinit var localParticipant: LocalParticipant
private set
private var mutableRemoteParticipants by flowDelegate(emptyMap<String, RemoteParticipant>())
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@get:FlowObservable
val remoteParticipants: Map<String, RemoteParticipant>
get() = mutableRemoteParticipants
private var mutableActiveSpeakers by flowDelegate(emptyList<Participant>())
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@get:FlowObservable
val activeSpeakers: List<Participant>
get() = mutableActiveSpeakers
... ...
... ... @@ -7,6 +7,7 @@ import io.livekit.android.room.track.LocalTrackPublication
import io.livekit.android.room.track.RemoteTrackPublication
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.TrackPublication
import io.livekit.android.util.FlowObservable
import io.livekit.android.util.flow
import io.livekit.android.util.flowDelegate
import kotlinx.coroutines.CoroutineDispatcher
... ... @@ -32,23 +33,27 @@ open class Participant(
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@get:FlowObservable
var participantInfo: LivekitModels.ParticipantInfo? by flowDelegate(null)
private set
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@get:FlowObservable
var identity: String? by flowDelegate(identity)
internal set
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@get:FlowObservable
var audioLevel: Float by flowDelegate(0f)
internal set
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@get:FlowObservable
var isSpeaking: Boolean by flowDelegate(false) { newValue, oldValue ->
if (newValue != oldValue) {
listener?.onSpeakingChanged(this)
... ... @@ -60,6 +65,7 @@ open class Participant(
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@get:FlowObservable
var metadata: String? by flowDelegate(null) { newMetadata, oldMetadata ->
if (newMetadata != oldMetadata) {
listener?.onMetadataChanged(this, oldMetadata)
... ... @@ -72,6 +78,7 @@ open class Participant(
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@get:FlowObservable
var connectionQuality by flowDelegate(ConnectionQuality.UNKNOWN)
internal set
... ... @@ -93,11 +100,13 @@ open class Participant(
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@get:FlowObservable
var tracks by flowDelegate(emptyMap<String, TrackPublication>())
protected set
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@get:FlowObservable
val audioTracks by flowDelegate(
stateFlow = ::tracks.flow
.map { it.filterValues { publication -> publication.kind == Track.Kind.AUDIO } }
... ... @@ -106,6 +115,7 @@ open class Participant(
/**
* Changes can be observed by using [io.livekit.android.util.flow]
*/
@get:FlowObservable
val videoTracks by flowDelegate(
stateFlow = ::tracks.flow
.map {
... ...
package io.livekit.android.room.track
import io.livekit.android.room.participant.Participant
import io.livekit.android.util.FlowObservable
import io.livekit.android.util.flowDelegate
import livekit.LivekitModels
import java.lang.ref.WeakReference
... ... @@ -10,6 +11,7 @@ open class TrackPublication(
track: Track?,
participant: Participant
) {
@get:FlowObservable
open var track: Track? by flowDelegate(track)
internal set
var name: String
... ...
... ... @@ -62,7 +62,16 @@ internal val <T> KProperty0<T>.delegate: Any?
val <T> KProperty0<T>.flow: StateFlow<T>
get() = delegate as StateFlow<T>
class MutableStateFlowDelegate<T>
/**
* Indicates that the target property changes can be observed with [flow].
*/
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
annotation class FlowObservable
@FlowObservable
internal class MutableStateFlowDelegate<T>
internal constructor(
private val flow: MutableStateFlow<T>,
private val onSetValue: ((newValue: T, oldValue: T) -> Unit)? = null
... ... @@ -82,7 +91,8 @@ internal constructor(
}
}
class StateFlowDelegate<T>
@FlowObservable
internal class StateFlowDelegate<T>
internal constructor(
private val flow: StateFlow<T>
) : StateFlow<T> by flow {
... ...
/build
\ No newline at end of file
... ...
plugins {
id 'java-library'
id 'kotlin'
id 'kotlin-kapt'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// used for lint rules
compileOnly deps.lintApi
compileOnly deps.lintChecks
compileOnly deps.lintTests
// Handle creating manifests for lint checker
compileOnly deps.auto.serviceAnnotations
kapt deps.auto.service
// test lint
testImplementation deps.lint
testImplementation deps.lintTests
compileOnly "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
// test runners
testImplementation deps.junit
testImplementation deps.junitJupiterApi
testRuntimeOnly deps.junitJupiterEngine
}
test {
environment "LINT_TEST_KOTLINC", ""
}
\ No newline at end of file
... ...
@file:Suppress("UnstableApiUsage") // We know that Lint API's aren't final.
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.livekit.lint
import com.android.tools.lint.detector.api.*
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiField
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallableReferenceExpression
import org.jetbrains.uast.UReferenceExpression
import org.jetbrains.uast.kotlin.KotlinUQualifiedReferenceExpression
import org.jetbrains.uast.tryResolve
/** Checks related to DiffUtil computation. */
class FlowDelegateUsageDetector : Detector(), SourceCodeScanner {
override fun visitReference(context: JavaContext, reference: UReferenceExpression, referenced: PsiElement) {
// Check if we're actually trying to access the flow delegate
val referencedMethod = referenced as? PsiMethod ?: return
if (referenced.name != GET_FLOW || referencedMethod.containingClass?.qualifiedName != FLOW_DELEGATE) {
return
}
// This should get the getter we're trying to receive the flow from.
val parent = reference.uastParent as? KotlinUQualifiedReferenceExpression
val rec = parent?.receiver
val resolve = rec?.tryResolve()
println("parent: $parent, rec: $rec, resolve: $resolve")
val receiver = ((reference.uastParent as? KotlinUQualifiedReferenceExpression)
?.receiver as? UCallableReferenceExpression)
?.resolve()
val isAnnotated = when (receiver) {
is PsiMethod -> {
println("${receiver.name}, ${receiver.annotations.fold("") { total, next -> "$total, ${next.text}" }}")
receiver.hasAnnotation(FLOW_OBSERVABLE_ANNOTATION)
}
is PsiField -> {
val receiverClass = (receiver.type as? PsiClassType)?.resolve()
println("${receiverClass}, ${receiverClass?.annotations?.fold("") { total, next -> "$total, ${next.text}" }}")
receiverClass?.hasAnnotation(FLOW_OBSERVABLE_ANNOTATION) ?: false
}
else -> {
false
}
}
if (!isAnnotated) {
val message = DEFAULT_MSG
val location = context.getLocation(reference)
context.report(ISSUE, reference, location, message)
}
}
override fun getApplicableReferenceNames(): List<String>? =
listOf("flow")
companion object {
// The name of the method for the flow accessor
private const val GET_FLOW = "getFlow"
// The containing file and implicitly generated class
private const val FLOW_DELEGATE = "io.livekit.android.util.FlowDelegateKt"
private const val FLOW_OBSERVABLE_ANNOTATION = "io.livekit.android.util.FlowObservable"
private const val DEFAULT_MSG =
"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."
private val IMPLEMENTATION =
Implementation(FlowDelegateUsageDetector::class.java, Scope.JAVA_FILE_SCOPE)
@JvmField
val ISSUE = Issue.create(
id = "FlowDelegateUsageDetector",
briefDescription = "flow on a non-@FlowObservable property",
explanation = """
Only properties marked with the @FlowObservable annotation can be observed using
`io.livekit.android.util.flow`.
""",
category = Category.CORRECTNESS,
priority = 4,
androidSpecific = true,
moreInfo = "https://issuetracker.google.com/116789824",
severity = Severity.ERROR,
implementation = IMPLEMENTATION
)
}
}
\ No newline at end of file
... ...
package io.livekit.lint
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.client.api.Vendor
import com.android.tools.lint.detector.api.CURRENT_API
import com.android.tools.lint.detector.api.Issue
import com.google.auto.service.AutoService
@Suppress("UnstableApiUsage", "unused")
@AutoService(value = [IssueRegistry::class])
class IssueRegistry : IssueRegistry() {
override val api: Int = CURRENT_API
override val vendor: Vendor = Vendor(
vendorName = "LiveKit",
identifier = "io.livekit.android",
feedbackUrl = "https://github.com/livekit/client-sdk-android",
)
override val issues: List<Issue>
get() = listOf(MediaTrackEqualsDetector.ISSUE, FlowDelegateUsageDetector.ISSUE)
}
\ No newline at end of file
... ...
@file:Suppress("UnstableApiUsage") // We know that Lint API's aren't final.
package io.livekit.lint
import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.*
import com.intellij.psi.PsiClassType
import org.jetbrains.uast.UBinaryExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UastBinaryOperator
/**
* Detects MediaStreamTrack.equals() usage. This is generally a mistake and should not be used.
*/
class MediaTrackEqualsDetector : Detector(), SourceCodeScanner {
override fun getApplicableUastTypes() =
listOf(UBinaryExpression::class.java, UCallExpression::class.java)
override fun createUastHandler(context: JavaContext): UElementHandler? {
return object : UElementHandler() {
override fun visitBinaryExpression(node: UBinaryExpression) {
checkExpression(context, node)
}
override fun visitCallExpression(node: UCallExpression) {
checkCall(context, node)
}
}
}
private fun checkCall(context: JavaContext, node: UCallExpression) {
if (node.methodName == "equals") {
val left = node.receiverType ?: return
val right = node.valueArguments.takeIf { it.isNotEmpty() }
?.get(0)
?.getExpressionType()
?: return
if (left is PsiClassType && right is PsiClassType
&& (left.canonicalText == MEDIA_STREAM_TRACK || right.canonicalText == MEDIA_STREAM_TRACK)
) {
val message = DEFAULT_MSG
val location = context.getLocation(node)
context.report(ISSUE, node, location, message)
}
}
}
private fun checkExpression(context: JavaContext, node: UBinaryExpression) {
if (node.operator == UastBinaryOperator.IDENTITY_EQUALS ||
node.operator == UastBinaryOperator.EQUALS
) {
val left = node.leftOperand.getExpressionType() ?: return
val right = node.rightOperand.getExpressionType() ?: return
if (left is PsiClassType && right is PsiClassType
&& (left.canonicalText == MEDIA_STREAM_TRACK || right.canonicalText == MEDIA_STREAM_TRACK)
) {
val message = DEFAULT_MSG
val location = node.operatorIdentifier?.let {
context.getLocation(it)
} ?: context.getLocation(node)
context.report(ISSUE, node, location, message)
}
}
}
companion object {
private const val MEDIA_STREAM_TRACK = "org.webrtc.MediaStreamTrack"
private const val DEFAULT_MSG =
"Suspicious equality check: MediaStreamTracks should not be checked for equality. Check id() instead."
private val IMPLEMENTATION =
Implementation(MediaTrackEqualsDetector::class.java, Scope.JAVA_FILE_SCOPE)
@JvmField
val ISSUE = Issue.create(
id = "MediaTrackEqualsDetector",
briefDescription = "Suspicious MediaStreamTrack Equality",
explanation = """
MediaStreamTrack does not implement `equals`, and therefore cannot be relied upon.
Additionally, many MediaStreamTrack objects may exist for the same underlying stream,
and therefore the identity operator `===` is unreliable.
""",
category = Category.CORRECTNESS,
priority = 4,
androidSpecific = true,
moreInfo = "https://github.com/livekit/client-sdk-android/commit/01152f2ac01dae59759383d587cdc21035718b8e",
severity = Severity.ERROR,
implementation = IMPLEMENTATION
)
}
}
\ No newline at end of file
... ...
@file:Suppress("UnstableApiUsage", "NewObjectEquality")
package io.livekit.lint
import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
import org.junit.Test
class FlowDelegateUsageDetectorTest {
@Test
fun normalFlowAccess() {
lint()
.allowMissingSdk()
.files(
flowAccess(),
stateFlow(),
kotlin(
"""
package foo
import io.livekit.android.util.FlowObservable
import io.livekit.android.util.flow
import io.livekit.android.util.flowDelegate
class Example {
@get:FlowObservable
val value: Int by flowDelegate(0)
fun foo() {
::value.flow
return
}
}"""
).indented()
)
.issues(FlowDelegateUsageDetector.ISSUE)
.run()
.expectClean()
}
@Test
fun thisColonAccess() {
lint()
.allowMissingSdk()
.files(
flowAccess(),
stateFlow(),
kotlin(
"""
package foo
import io.livekit.android.util.FlowObservable
import io.livekit.android.util.flow
import io.livekit.android.util.flowDelegate
class Example {
@get:FlowObservable
val value: Int by flowDelegate(0)
fun foo() {
this::value.flow
return
}
}"""
).indented()
)
.issues(FlowDelegateUsageDetector.ISSUE)
.run()
.expectClean()
}
@Test
fun otherClassAccess() {
lint()
.allowMissingSdk()
.files(
flowAccess(),
stateFlow(),
kotlin(
"""
package foo
import io.livekit.android.util.FlowObservable
import io.livekit.android.util.flow
import io.livekit.android.util.flowDelegate
class FlowContainer {
@get:FlowObservable
val value: Int by flowDelegate(0)
}
class Example {
fun foo() {
val container = FlowContainer()
container::value.flow
return
}
}"""
).indented()
)
.issues(FlowDelegateUsageDetector.ISSUE)
.run()
.expectClean()
}
@Test
fun parenthesesClassAccess() {
lint()
.allowMissingSdk()
.files(
flowAccess(),
stateFlow(),
kotlin(
"""
package foo
import io.livekit.android.util.FlowObservable
import io.livekit.android.util.flow
import io.livekit.android.util.flowDelegate
class FlowContainer {
@get:FlowObservable
val value: Int by flowDelegate(0)
}
class Example {
fun foo() {
val container = FlowContainer()
(container)::value.flow
return
}
}"""
).indented()
)
.issues(FlowDelegateUsageDetector.ISSUE)
.run()
.expectClean()
}
@Test
fun roundaboutAccess() {
lint()
.allowMissingSdk()
.files(
flowAccess(),
stateFlow(),
kotlin(
"""
package foo
import io.livekit.android.util.FlowObservable
import io.livekit.android.util.flow
import io.livekit.android.util.flowDelegate
class FlowContainer {
var value: Int by flowDelegate(0)
@get:FlowObservable
val otherValue: Int
get() = value
}
class Example {
fun foo() {
val container = FlowContainer()
container::otherValue.flow
return
}
}"""
).indented()
)
.issues(FlowDelegateUsageDetector.ISSUE)
.run()
.expectClean()
}
@Test
fun nonAnnotatedFlowAccess() {
lint()
.allowMissingSdk()
.files(
flowAccess(),
stateFlow(),
kotlin(
"""
package foo
import io.livekit.android.util.FlowObservable
import io.livekit.android.util.flow
import io.livekit.android.util.flowDelegate
class Example {
val value: Int = 0
fun foo() {
this::value.flow
return
}
}"""
).indented()
)
.issues(FlowDelegateUsageDetector.ISSUE)
.run()
.expectErrorCount(1)
}
}
fun flowAccess(): TestFile {
return kotlin(
"io/livekit/android/util/FlowDelegate.kt",
"""
package io.livekit.android.util
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty0
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.MutableStateFlow
internal val <T> KProperty0<T>.delegate: Any?
get() { getDelegate() }
@Suppress("UNCHECKED_CAST")
val <T> KProperty0<T>.flow: StateFlow<T>
get() = delegate as StateFlow<T>
@Target(AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class FlowObservable
@FlowObservable
class MutableStateFlowDelegate<T>
internal constructor(
private val flow: MutableStateFlow<T>,
private val onSetValue: ((newValue: T, oldValue: T) -> Unit)? = null
) : MutableStateFlow<T> by flow {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return flow.value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
val oldValue = flow.value
flow.value = value
onSetValue?.invoke(value, oldValue)
}
}
public fun <T> flowDelegate(
initialValue: T,
onSetValue: ((newValue: T, oldValue: T) -> Unit)? = null
): MutableStateFlowDelegate<T> {
return MutableStateFlowDelegate(MutableStateFlow(initialValue), onSetValue)
}
"""
).indented()
.within("src")
}
fun stateFlow(): TestFile {
return kotlin(
"""
package kotlinx.coroutines.flow
interface StateFlow<out T> {
val value: T
}
class MutableStateFlow<T>(override var value: T) : StateFlow<T>
"""
).indented()
}
\ No newline at end of file
... ...
@file:Suppress("UnstableApiUsage", "NewObjectEquality")
package io.livekit.lint
import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.checks.infrastructure.TestFiles.java
import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
import org.junit.Test
class MediaTrackEqualsDetectorTest {
@Test
fun objectEquals() {
lint()
.allowMissingSdk()
.files(
java(
"""
package foo;
class Example {
public boolean foo() {
Object a = new Object();
Object b = new Object();
return a.equals(b);
}
}"""
).indented()
)
.issues(MediaTrackEqualsDetector.ISSUE)
.run()
.expectClean()
}
@Test
fun objectEqualityOperator() {
lint()
.allowMissingSdk()
.files(
java(
"""
package foo;
class Example {
public boolean foo() {
Object a = new Object();
Object b = new Object();
return a == b;
}
}"""
).indented()
)
.issues(MediaTrackEqualsDetector.ISSUE)
.run()
.expectClean()
}
@Test
fun badMediaTrackEquals() {
lint()
.allowMissingSdk()
.files(
mediaStreamTrack(),
java(
"""
package foo;
import org.webrtc.MediaStreamTrack;
class Example {
public boolean foo() {
MediaStreamTrack a = new MediaStreamTrack();
MediaStreamTrack b = new MediaStreamTrack();
return a.equals(b);
}
}"""
).indented()
)
.issues(MediaTrackEqualsDetector.ISSUE)
.run()
.expectErrorCount(1)
}
@Test
fun mediaTrackEqualityOperator() {
lint()
.allowMissingSdk()
.files(
mediaStreamTrack(),
java(
"""
package foo;
import org.webrtc.MediaStreamTrack;
class Example {
public boolean foo() {
ABC a = new ABC();
MediaStreamTrack b = new MediaStreamTrack();
return a == b;
}
public boolean equals(Object o){
return false;
}
}"""
).indented()
)
.issues(MediaTrackEqualsDetector.ISSUE)
.run()
.expectErrorCount(1)
}
@Test
fun properMediaTrackEquality() {
lint()
.allowMissingSdk()
.files(
mediaStreamTrack(),
java(
"""
package foo;
class Example {
public boolean foo() {
MediaStreamTrack a = new MediaStreamTrack();
MediaStreamTrack b = new MediaStreamTrack();
return a.getId() == b.getId();
}
}"""
).indented()
)
.issues(MediaTrackEqualsDetector.ISSUE)
.run()
.expectClean()
}
@Test
fun kotlinMediaTrackEqualityOperator() {
lint()
.allowMissingSdk()
.files(
mediaStreamTrack(),
kotlin(
"""
package foo
import org.webrtc.MediaStreamTrack
class Example {
fun foo() : Boolean {
val a = MediaStreamTrack()
val b = MediaStreamTrack()
return a == b;
}
}"""
).indented()
)
.issues(MediaTrackEqualsDetector.ISSUE)
.run()
.expectErrorCount(1)
}
@Test
fun kotlinMediaTrackIdentityEqualityOperator() {
lint()
.allowMissingSdk()
.files(
mediaStreamTrack(),
kotlin(
"""
package foo
import org.webrtc.MediaStreamTrack
class Example {
fun foo() : Boolean {
val a = MediaStreamTrack()
val b = MediaStreamTrack()
return a === b
}
}"""
).indented()
)
.issues(MediaTrackEqualsDetector.ISSUE)
.run()
.expectErrorCount(1)
}
@Test
fun kotlinMediaTrackEquals() {
lint()
.allowMissingSdk()
.files(
mediaStreamTrack(),
kotlin(
"""
package foo
import org.webrtc.MediaStreamTrack
class Example {
fun foo() : Boolean {
val a = MediaStreamTrack()
val b = MediaStreamTrack()
return a.equals(b)
}
}"""
).indented()
)
.issues(MediaTrackEqualsDetector.ISSUE)
.run()
.expectErrorCount(1)
}
@Test
fun kotlinProperMediaTrackEquality() {
lint()
.allowMissingSdk()
.files(
mediaStreamTrack(),
kotlin(
"""
package foo
import org.webrtc.MediaStreamTrack
class Example {
fun foo() : Boolean {
val a = MediaStreamTrack()
val b = MediaStreamTrack()
return a.id() == b.id()
}
}"""
).indented()
)
.issues(MediaTrackEqualsDetector.ISSUE)
.run()
.expectClean()
}
}
fun mediaStreamTrack(): TestFile {
return java(
"""
package org.webrtc;
class MediaStreamTrack {
int getId(){
return 0;
}
}
"""
).indented()
}
\ No newline at end of file
... ...
... ... @@ -7,3 +7,4 @@ pluginManagement {
include ':sample-app', ':sample-app-compose', ':livekit-android-sdk'
rootProject.name='livekit-android'
include ':sample-app-common'
include ':livekit-lint'
... ...