David Liu

Temp commit

1 <?xml version="1.0" encoding="UTF-8"?> 1 <?xml version="1.0" encoding="UTF-8"?>
2 <project version="4"> 2 <project version="4">
3 <component name="CompilerConfiguration"> 3 <component name="CompilerConfiguration">
4 - <bytecodeTargetLevel target="11" /> 4 + <bytecodeTargetLevel target="1.8">
  5 + <module name="livekit-android.livekit-android-sdk" target="11" />
  6 + <module name="livekit-android.sample-app" target="11" />
  7 + </bytecodeTargetLevel>
5 </component> 8 </component>
6 </project> 9 </project>
@@ -12,7 +12,7 @@ buildscript { @@ -12,7 +12,7 @@ buildscript {
12 } 12 }
13 dependencies { 13 dependencies {
14 classpath 'com.android.tools.build:gradle:7.0.1' 14 classpath 'com.android.tools.build:gradle:7.0.1'
15 - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 15 + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30"
16 classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" 16 classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
17 classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" 17 classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
18 classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.15' 18 classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.15'
@@ -52,12 +52,25 @@ ext { @@ -52,12 +52,25 @@ ext {
52 androidx_lifecycle: "2.3.1", 52 androidx_lifecycle: "2.3.1",
53 dagger : "2.27", 53 dagger : "2.27",
54 groupie : "2.9.0", 54 groupie : "2.9.0",
  55 + junit : "4.13.2",
  56 + junitJupiter : "5.5.0",
  57 + lint : "30.0.1",
55 protobuf : "3.15.1", 58 protobuf : "3.15.1",
56 ] 59 ]
57 generated = [ 60 generated = [
58 protoSrc: "$projectDir/../protocol", 61 protoSrc: "$projectDir/../protocol",
59 ] 62 ]
60 deps = [ 63 deps = [
  64 + // lint
  65 + lint : "com.android.tools.lint:lint:${versions.lint}",
  66 + lintApi : "com.android.tools.lint:lint-api:${versions.lint}",
  67 + lintChecks : "com.android.tools.lint:lint-checks:${versions.lint}",
  68 + lintTests : "com.android.tools.lint:lint-tests:${versions.lint}",
  69 +
  70 + // tests
  71 + junit : "junit:junit:${versions.junit}",
  72 + junitJupiterApi : "org.junit.jupiter:junit-jupiter-api:${versions.junitJupiter}",
  73 + junitJupiterEngine: "org.junit.jupiter:junit-jupiter-engine:${versions.junitJupiter}",
61 ] 74 ]
62 annotations = [ 75 annotations = [
63 ] 76 ]
  1 +plugins {
  2 + id 'java-library'
  3 + id 'kotlin'
  4 +}
  5 +
  6 +java {
  7 + sourceCompatibility = JavaVersion.VERSION_1_8
  8 + targetCompatibility = JavaVersion.VERSION_1_8
  9 +}
  10 +
  11 +dependencies {
  12 + // used for lint rules
  13 + compileOnly deps.lintApi
  14 + compileOnly deps.lintChecks
  15 + compileOnly deps.lintTests
  16 +
  17 + // test lint
  18 + testImplementation deps.lintTests
  19 +
  20 + // test runners
  21 + testImplementation deps.junitJupiterApi
  22 + testRuntimeOnly deps.junitJupiterEngine
  23 +}
  1 +package io.livekit.lint
  2 +
  3 +import com.android.tools.lint.client.api.IssueRegistry
  4 +import com.android.tools.lint.detector.api.CURRENT_API
  5 +import com.android.tools.lint.detector.api.Issue
  6 +
  7 +class IssueRegistry : IssueRegistry() {
  8 +
  9 + override val api: Int = CURRENT_API
  10 +
  11 + override val issues: List<Issue>
  12 + get() = listOf(MediaTrackEquals.ISSUE)
  13 +}
  1 +/*
  2 + * Copyright (C) 2018 The Android Open Source Project
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.lint
  18 +
  19 +import com.android.tools.lint.checks.DiffUtilDetector
  20 +import com.android.tools.lint.detector.api.Category
  21 +import com.android.tools.lint.detector.api.Detector
  22 +import com.android.tools.lint.detector.api.Implementation
  23 +import com.android.tools.lint.detector.api.Issue
  24 +import com.android.tools.lint.detector.api.JavaContext
  25 +import com.android.tools.lint.detector.api.Scope
  26 +import com.android.tools.lint.detector.api.Severity
  27 +import com.android.tools.lint.detector.api.SourceCodeScanner
  28 +import com.android.tools.lint.detector.api.isKotlin
  29 +import com.intellij.psi.CommonClassNames.JAVA_LANG_OBJECT
  30 +import com.intellij.psi.PsiClassType
  31 +import com.intellij.psi.PsiElement
  32 +import com.intellij.psi.PsiMethod
  33 +import org.jetbrains.uast.UBinaryExpression
  34 +import org.jetbrains.uast.UBinaryExpressionWithType
  35 +import org.jetbrains.uast.UCallExpression
  36 +import org.jetbrains.uast.UClass
  37 +import org.jetbrains.uast.UElement
  38 +import org.jetbrains.uast.UExpression
  39 +import org.jetbrains.uast.UIfExpression
  40 +import org.jetbrains.uast.UMethod
  41 +import org.jetbrains.uast.UParenthesizedExpression
  42 +import org.jetbrains.uast.UPolyadicExpression
  43 +import org.jetbrains.uast.UQualifiedReferenceExpression
  44 +import org.jetbrains.uast.UastBinaryOperator
  45 +import org.jetbrains.uast.getParentOfType
  46 +import org.jetbrains.uast.skipParenthesizedExprUp
  47 +import org.jetbrains.uast.tryResolve
  48 +import org.jetbrains.uast.visitor.AbstractUastVisitor
  49 +
  50 +/** Checks related to DiffUtil computation. */
  51 +class MediaTrackEquals : Detector(), SourceCodeScanner {
  52 +
  53 + override fun visitClass(context: JavaContext, declaration: UClass) {
  54 + for (method in declaration.methods) {
  55 + checkMethod(context, method)
  56 + }
  57 + }
  58 +
  59 + private fun checkMethod(
  60 + context: JavaContext,
  61 + declaration: UMethod
  62 + ) {
  63 + declaration.accept(object : AbstractUastVisitor() {
  64 + override fun visitBinaryExpression(node: UBinaryExpression): Boolean {
  65 + checkExpression(context, node)
  66 + return super.visitBinaryExpression(node)
  67 + }
  68 +
  69 + override fun visitCallExpression(node: UCallExpression): Boolean {
  70 + checkCall(context, node)
  71 + return super.visitCallExpression(node)
  72 + }
  73 + })
  74 + }
  75 +
  76 + private fun defaultEquals(context: JavaContext, node: UElement): Boolean {
  77 + val resolved: PsiMethod?
  78 +
  79 + when (node) {
  80 + is UBinaryExpression -> {
  81 + resolved = node.resolveOperator()
  82 + if (resolved == null) {
  83 + val left = node.leftOperand.getExpressionType() as? PsiClassType
  84 + return defaultEquals(context, left)
  85 + }
  86 + }
  87 + is UCallExpression -> {
  88 + resolved = node.resolve()
  89 + }
  90 + is UParenthesizedExpression -> {
  91 + return defaultEquals(context, node.expression)
  92 + }
  93 + else -> {
  94 + // We don't know any better
  95 + return false
  96 + }
  97 + }
  98 +
  99 + return resolved?.containingClass?.qualifiedName == JAVA_LANG_OBJECT
  100 + }
  101 +
  102 + private fun defaultEquals(
  103 + context: JavaContext,
  104 + type: PsiClassType?
  105 + ): Boolean {
  106 + val cls = type?.resolve() ?: return false
  107 +
  108 + if (isKotlin(cls) && (context.evaluator.isSealed(cls) || context.evaluator.isData(cls))) {
  109 + // Sealed class doesn't guarantee that it defines equals/hashCode
  110 + // but it's likely (we'd need to go look at each inner class)
  111 + return false
  112 + }
  113 +
  114 + for (m in cls.findMethodsByName("equals", true)) {
  115 + if (m is PsiMethod) {
  116 + val parameters = m.parameterList.parameters
  117 + if (parameters.size == 1 &&
  118 + parameters[0].type.canonicalText == JAVA_LANG_OBJECT
  119 + ) {
  120 + return m.containingClass?.qualifiedName == JAVA_LANG_OBJECT
  121 + }
  122 + }
  123 + }
  124 +
  125 + return false
  126 + }
  127 +
  128 + private fun checkCall(context: JavaContext, node: UCallExpression) {
  129 + if (defaultEquals(context, node)) {
  130 + // Within cast or instanceof check which implies a more specific type
  131 + // which provides an equals implementation?
  132 + if (withinCastWithEquals(context, node)) {
  133 + return
  134 + }
  135 +
  136 + val targetType = node.receiverType?.canonicalText ?: "target"
  137 + val message = "Suspicious equality check: `equals()` is not implemented in $targetType"
  138 + val location = context.getCallLocation(node, false, true)
  139 + context.report(ISSUE, node, location, message)
  140 + }
  141 + }
  142 +
  143 + /**
  144 + * Is this .equals() call within another if check which checks
  145 + * instanceof on a more specific type than we're calling equals on?
  146 + * If so, does that more specific type define its own equals?
  147 + *
  148 + * Also handle an implicit check via short circuit evaluation; e.g.
  149 + * something like "return a is A && b is B && a.equals(b)".
  150 + */
  151 + private fun withinCastWithEquals(context: JavaContext, node: UExpression): Boolean {
  152 + var parent = skipParenthesizedExprUp(node.uastParent)
  153 + if (parent is UQualifiedReferenceExpression) {
  154 + parent = skipParenthesizedExprUp(parent.uastParent)
  155 + }
  156 + val target: PsiElement? = when (node) {
  157 + is UCallExpression -> node.receiver?.tryResolve()
  158 + is UBinaryExpression -> node.leftOperand.tryResolve()
  159 + else -> null
  160 + }
  161 +
  162 + if (parent is UPolyadicExpression && parent.operator == UastBinaryOperator.LOGICAL_AND) {
  163 + val operands = parent.operands
  164 + for (operand in operands) {
  165 + if (operand === node) {
  166 + break
  167 + }
  168 + if (isCastWithEquals(context, operand, target)) {
  169 + return true
  170 + }
  171 + }
  172 + }
  173 + val ifStatement = node.getParentOfType<UElement>(UIfExpression::class.java, false, UMethod::class.java)
  174 + as? UIfExpression ?: return false
  175 + val condition = ifStatement.condition
  176 + return isCastWithEquals(context, condition, target)
  177 + }
  178 +
  179 + private fun isCastWithEquals(context: JavaContext, node: UExpression, target: PsiElement?): Boolean {
  180 + when {
  181 + node is UBinaryExpressionWithType -> {
  182 + if (target != null) {
  183 + val resolved = node.operand.tryResolve()
  184 + // Unfortunately in some scenarios isEquivalentTo returns false for equal instances
  185 + //noinspection LintImplPsiEquals
  186 + if (resolved != null && !(target == resolved || target.isEquivalentTo(resolved))) {
  187 + return false
  188 + }
  189 + }
  190 + return !defaultEquals(context, node.type as? PsiClassType)
  191 + }
  192 + node is UPolyadicExpression && node.operator == UastBinaryOperator.LOGICAL_AND -> {
  193 + for (operand in node.operands) {
  194 + if (isCastWithEquals(context, operand, target)) {
  195 + return true
  196 + }
  197 + }
  198 + }
  199 + node is UParenthesizedExpression -> {
  200 + return isCastWithEquals(context, node.expression, target)
  201 + }
  202 + }
  203 + return false
  204 + }
  205 +
  206 + private fun checkExpression(context: JavaContext, node: UBinaryExpression) {
  207 + if (node.operator == UastBinaryOperator.IDENTITY_EQUALS ||
  208 + node.operator == UastBinaryOperator.EQUALS
  209 + ) {
  210 + val left = node.leftOperand.getExpressionType() ?: return
  211 + val right = node.rightOperand.getExpressionType() ?: return
  212 + if (left is PsiClassType && right is PsiClassType) {
  213 + if (node.operator == UastBinaryOperator.EQUALS) {
  214 + if (defaultEquals(context, node)) {
  215 + if (withinCastWithEquals(context, node)) {
  216 + return
  217 + }
  218 +
  219 + val message =
  220 + "Suspicious equality check: `equals()` is not implemented in ${left.className}"
  221 + val location = node.operatorIdentifier?.let {
  222 + context.getLocation(it)
  223 + } ?: context.getLocation(node)
  224 + context.report(ISSUE, node, location, message)
  225 + }
  226 + } else {
  227 + val message = if (isKotlin(node.sourcePsi))
  228 + "Suspicious equality check: Did you mean `==` instead of `===` ?"
  229 + else
  230 + "Suspicious equality check: Did you mean `.equals()` instead of `==` ?"
  231 + val location = node.operatorIdentifier?.let {
  232 + context.getLocation(it)
  233 + } ?: context.getLocation(node)
  234 + context.report(ISSUE, node, location, message)
  235 + }
  236 + }
  237 + }
  238 + }
  239 +
  240 + companion object {
  241 + private val IMPLEMENTATION =
  242 + Implementation(DiffUtilDetector::class.java, Scope.JAVA_FILE_SCOPE)
  243 +
  244 + @JvmField
  245 + val ISSUE = Issue.create(
  246 + id = "DiffUtilEquals",
  247 + briefDescription = "Suspicious DiffUtil Equality",
  248 + explanation = """
  249 + `areContentsTheSame` is used by `DiffUtil` to produce diffs. If the \
  250 + method is implemented incorrectly, such as using identity equals \
  251 + instead of equals, or calling equals on a class that has not implemented \
  252 + it, weird visual artifacts can occur.
  253 + """,
  254 + category = Category.CORRECTNESS,
  255 + priority = 4,
  256 + androidSpecific = true,
  257 + moreInfo = "https://issuetracker.google.com/116789824",
  258 + severity = Severity.ERROR,
  259 + implementation = IMPLEMENTATION
  260 + )
  261 + }
  262 +}
@@ -6,3 +6,4 @@ pluginManagement { @@ -6,3 +6,4 @@ pluginManagement {
6 } 6 }
7 include ':sample-app', ':livekit-android-sdk' 7 include ':sample-app', ':livekit-android-sdk'
8 rootProject.name='livekit-android' 8 rootProject.name='livekit-android'
  9 +include ':livekit-lint'