blob: ae489d8c201a15e4db0934bef6c3f56bc192debf [file] [log] [blame]
/*
* 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 com.android.tools.lint.detector.api
import com.android.ide.common.rendering.api.ResourceNamespace
import com.android.resources.ResourceType
import com.android.tools.lint.checks.AbstractCheckTest
import com.android.tools.lint.checks.infrastructure.TestMode
import com.android.tools.lint.client.api.ResourceRepositoryScope
import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.Context.Companion.isSuppressedWithComment
import com.intellij.psi.PsiMethod
import java.util.EnumSet
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UElement
import org.jetbrains.uast.ULiteralExpression
import org.jetbrains.uast.getValueIfStringLiteral
class ContextTest : AbstractCheckTest() {
fun testSuppressFileAnnotation() {
// Regression test for https://issuetracker.google.com/116838536
lint()
.files(
kotlin(
"""
@file:Suppress("unused", "_TestIssueId")
package test.pkg
class MyTest {
val s: String = "/sdcard/mydir"
}
"""
)
.indented(),
gradle(""),
)
.issues(TEST_ISSUE)
.run()
.expectClean()
}
fun testSuppressLine() {
// Issue id
assertFalse(isSuppressedWithComment("", TEST_ISSUE))
assertFalse(isSuppressedWithComment("A_TestIssueId", TEST_ISSUE))
assertFalse(isSuppressedWithComment("_TestIssueIdB", TEST_ISSUE))
assertTrue(isSuppressedWithComment("_TestIssueId", TEST_ISSUE))
assertTrue(isSuppressedWithComment("_TestIssueIdFooBar,_TestIssueId", TEST_ISSUE))
assertTrue(isSuppressedWithComment("/**@noinspection _TestIssueId*/", TEST_ISSUE))
assertTrue(isSuppressedWithComment("[@noinspection _TestIssueId]", TEST_ISSUE))
assertTrue(isSuppressedWithComment("<!--noinspection _TestIssueId-->", TEST_ISSUE))
assertTrue(isSuppressedWithComment(" _TestIssueId ", TEST_ISSUE))
assertTrue(isSuppressedWithComment(" _testissueid ", TEST_ISSUE))
assertTrue(isSuppressedWithComment("A, _TestIssueId", TEST_ISSUE))
assertTrue(isSuppressedWithComment("_TestIssueId, B", TEST_ISSUE))
assertTrue(isSuppressedWithComment("A, _TestIssueId, B", TEST_ISSUE))
// Category
assertFalse(isSuppressedWithComment("AMessages", TEST_ISSUE))
assertFalse(isSuppressedWithComment("MessagesB", TEST_ISSUE))
assertTrue(isSuppressedWithComment("Messages", TEST_ISSUE))
assertTrue(isSuppressedWithComment(" Messages ", TEST_ISSUE))
assertTrue(isSuppressedWithComment("Correctness", TEST_ISSUE))
assertTrue(isSuppressedWithComment("Correctness:Messages", TEST_ISSUE))
assertTrue(isSuppressedWithComment("A,Messages", TEST_ISSUE))
assertTrue(isSuppressedWithComment("Messages,B", TEST_ISSUE))
assertTrue(isSuppressedWithComment("A, Messages, B", TEST_ISSUE))
}
fun testSuppressObjectAnnotation() {
// Regression test for https://issuetracker.google.com/116838536
lint()
.files(
kotlin(
"""
package test.pkg
import android.annotation.SuppressLint
@SuppressLint("_TestIssueId")
object TestClass1 {
const val s: String = "/sdcard/mydir"
}"""
)
.indented(),
gradle(""),
)
.issues(TEST_ISSUE)
.run()
.expectClean()
}
fun testSuppressCompanionObjectAnnotation() {
// Regression test for b/293334438
lint()
.files(
kotlin(
"""
package test.pkg
import android.annotation.SuppressLint
class MyClass {
@SuppressLint("_TestIssueId")
companion object {
const val s: String = "/sdcard/mydir"
}
}
"""
)
.indented(),
gradle(""),
)
.issues(TEST_ISSUE)
.run()
.expectClean()
}
fun testSuppressPropertyAnnotation() {
// Regression test for b/296288411
lint()
.files(
kotlin(
"""
package test.pkg
import android.annotation.SuppressLint
class MyClass {
@SuppressLint("_TestIssueId") val s: String get() = {
class TestClass1 {
const val s: String = "/sdcard/mydir"
}
TestClass1().s
}
}
"""
)
.indented(),
gradle(""),
)
.issues(TEST_ISSUE)
.run()
.expectClean()
}
fun testKotlinSuppressionAnnotationsWithPsiScope() {
// Regression test for b/274787712
// When a PsiElement is passed as the scope for an incident, LintDriver behaves differently.
// In particular, it needs to implement the same logic for both Kotlin and Java PSI, which it
// was not doing when b/274787712 was reported.
lint()
.files(
kotlin(
"""
package test.pkg
import android.annotation.SuppressLint
class MyClass {
@SuppressLint("_PsiTestIssueId")
const val s: String = "/sdcard/mydir"
}
"""
)
.indented(),
gradle(""),
)
.issues(PSI_TEST_ISSUE)
.run()
.expectClean()
}
fun testMultilineReporter() {
// Test to make sure that when the argument to string is indented and/or has line continuations
// (\) the
// message is properly processed.
lint()
.files(
kotlin(
"""
fun method() {
method() // ERROR
}
"""
)
.indented()
)
.issues(MultiLineReporter.ISSUE)
.run()
.expect(
"""
src/test.kt:2: Warning: Error message indented and split across multiple lines. [_MultilineReporter]
method() // ERROR
~~~~~~~~
0 errors, 1 warnings
"""
)
}
class MultiLineReporter : Detector(), SourceCodeScanner {
override fun getApplicableMethodNames() = listOf("method")
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
context.report(
Incident(
ISSUE,
node,
context.getLocation(node),
"""
Error message indented and split across \
multiple lines.
""",
)
)
}
companion object {
val ISSUE =
Issue.create(
"_MultilineReporter",
"Not applicable",
"Not applicable",
Category.MESSAGES,
5,
Severity.WARNING,
Implementation(MultiLineReporter::class.java, Scope.JAVA_FILE_SCOPE),
)
}
}
fun testLocationOfKotlinString() {
val tripleQuotes = "\"\"\""
lint()
.files(
kotlin(
"""
package com.example
class MyClass {
fun foo(arg1: String) {
}
fun bar() {
val a = 5
foo(arg1 = "hello")
foo(arg1 = "hello" + " world")
foo(arg1 = "")
foo(arg1 = "${a}")
foo(arg1 = "${a} ")
foo(arg1 = ${tripleQuotes}hello${tripleQuotes})
}
}
"""
)
.indented()
)
.issues(ReportsArgumentDetector.ISSUE)
.run()
.expect(
"""
src/com/example/MyClass.kt:11: Warning: Argument to foo [_UReportsArgumentIssue]
foo(arg1 = "hello")
~~~~~~~
src/com/example/MyClass.kt:12: Warning: Argument to foo [_UReportsArgumentIssue]
foo(arg1 = "hello" + " world")
~~~~~~~~~~~~~~~~~~
src/com/example/MyClass.kt:13: Warning: Argument to foo [_UReportsArgumentIssue]
foo(arg1 = "")
~~
src/com/example/MyClass.kt:14: Warning: Argument to foo [_UReportsArgumentIssue]
foo(arg1 = "${a}")
~~~~~~
src/com/example/MyClass.kt:15: Warning: Argument to foo [_UReportsArgumentIssue]
foo(arg1 = "${a} ")
~~~~~~~
src/com/example/MyClass.kt:16: Warning: Argument to foo [_UReportsArgumentIssue]
foo(arg1 = ${tripleQuotes}hello${tripleQuotes})
~~~~~~~~~~~
0 errors, 6 warnings
"""
)
}
class ReportsArgumentDetector : Detector(), SourceCodeScanner {
override fun getApplicableMethodNames() = listOf("foo")
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
val arg = node.getArgumentForParameter(0)
context.report(Incident(ISSUE, "Argument to foo", context.getLocation(arg), arg))
}
companion object {
val ISSUE =
Issue.create(
"_UReportsArgumentIssue",
"Not applicable",
"Not applicable",
Category.MESSAGES,
5,
Severity.WARNING,
Implementation(ReportsArgumentDetector::class.java, Scope.JAVA_FILE_SCOPE),
)
}
}
fun testSuppressKotlinViaGradleContext() {
// The ReportsUElementFromGradleContextDetector stores the call to foo() (a UElement) in a field
// and then reports a Gradle element, but passes the UElement as the scope (so that the user
// could suppress the warning by adding an annotation to the foo() call). It is unclear whether
// this is really a good idea, but there are detectors out there that already do this, so lint
// should at least not fail. This would previously cause a ClassCastException because the scope
// was assumed to be a Gradle element, and was unsafely cast.
// Regression test for b/296986527 and b/293517205
lint()
.files(
kotlin(
"""
package test.pkg
class MyClass {
fun foo() {
}
fun bar() {
foo()
}
}
"""
)
.indented(),
gradle(
"""
android {
defaultConfig {
applicationId "com.android.tools.test"
}
}
"""
)
.indented(),
)
.issues(ReportsUElementFromGradleContextDetector.ISSUE)
.run()
.expect(
"""
build.gradle:3: Warning: Bad [_UElementIssue]
applicationId "com.android.tools.test"
~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 1 warnings
"""
)
}
fun testAccessLibraryResource() {
// Makes sure we complain about accessing library resources in partial analysis mode
val lib =
project(
xml(
"res/values/string.xml",
"""
<resources>
<string name="lib">Library Resource</string>
</resources>
""",
)
.indented()
)
val main =
project(
xml(
"res/values/string.xml",
"""
<resources>
<string name="local">Local Resource</string>
</resources>
""",
)
.indented(),
kotlin(
"""
private const val s = "testAccessLibraryResource"// Triggers detector to look up resources
"""
)
.indented(),
)
.dependsOn(lib)
lint()
.issues(TEST_ISSUE)
.projects(lib, main)
// We only care about partial mode where accessing library resources
// should trigger an error in the analysis phase
.testModes(TestMode.PARTIAL)
.run()
.expectContains(
"""
../lib/res/values/string.xml: Error: The lint detector
com.android.tools.lint.detector.api.ContextTest$NoLocationNodeDetector
called ResourceItem.getSource() during module analysis.
This does not work correctly when running in test.
You can only call this on resources in the current module, not library resources.
In particular, there may be false positives or false negatives because
the lint check may be using the minSdkVersion or manifest information
from the library instead of any consuming app module.
Contact the vendor of the lint issue to get it fixed/updated (if
known, listed below), and in the meantime you can try to work around
this by disabling the following issues:
"_TestIssueId"
Issue Vendors:
Call stack: LintResourceRepository$Companion$removeFileAccess$withoutSource$1.reportPathAccess(LintResourceRepository.kt
"""
)
}
fun testAccessMainProject() {
// Makes sure we complain about accessing the main project in analysis mode
lint()
.files(
kotlin(
"""
private const val s = "testAccessMainProject"// Triggers detector to access the main project
"""
)
.indented()
)
.issues(TEST_ISSUE)
// We only care about partial mode where accessing library resources
// should trigger an error in the analysis phase
.testModes(TestMode.PARTIAL)
.run()
.expectContains(
"""
src/test.kt: Error: The lint detector
com.android.tools.lint.detector.api.ContextTest$NoLocationNodeDetector
called context.getMainProject() during module analysis.
This does not work correctly when running in Lint Unit Tests.
In particular, there may be false positives or false negatives because
the lint check may be using the minSdkVersion or manifest information
from the library instead of any consuming app module.
Contact the vendor of the lint issue to get it fixed/updated (if
known, listed below), and in the meantime you can try to work around
this by disabling the following issues:
"_TestIssueId"
Issue Vendors:
Call stack: Context$Companion.checkForbidden$default(Context.kt:
"""
)
}
class ReportsUElementFromGradleContextDetector : Detector(), SourceCodeScanner, GradleScanner {
// See testSuppressKotlinViaGradleContext.
var element: UElement? = null
override fun getApplicableMethodNames() = listOf("foo")
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
element = node
}
override fun checkDslPropertyAssignment(
context: GradleContext,
property: String,
value: String,
parent: String,
parentParent: String?,
propertyCookie: Any,
valueCookie: Any,
statementCookie: Any,
) {
context.report(Incident(ISSUE, element!!, context.getLocation(valueCookie), "Bad"))
}
companion object {
val ISSUE =
Issue.create(
"_UElementIssue",
"Not applicable",
"Not applicable",
Category.MESSAGES,
5,
Severity.WARNING,
Implementation(
ReportsUElementFromGradleContextDetector::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.GRADLE_FILE),
),
)
}
}
override fun getDetector(): Detector = NoLocationNodeDetector()
override fun getIssues(): List<Issue> = listOf(TEST_ISSUE, PSI_TEST_ISSUE)
// Detector which reproduces problem in issue https://issuetracker.google.com/116838536
class NoLocationNodeDetector : Detector(), SourceCodeScanner {
override fun getApplicableUastTypes(): List<Class<out UElement>>? =
listOf(ULiteralExpression::class.java)
override fun createUastHandler(context: JavaContext): UElementHandler? =
object : UElementHandler() {
override fun visitLiteralExpression(node: ULiteralExpression) {
val s = node.getValueIfStringLiteral()
if (s != null && s.startsWith("/sdcard/")) {
val message = """Sample error message"""
val location = context.getLocation(node)
// Note: We're calling
// context.report(Issue, Location, String)
// NOT:
// context.report(Issue, UElement, Location, String)
// to test that we suppress based on stashed location
// source element from above; this tests issue 116838536
context.report(TEST_ISSUE, location, message)
// If we pass a PsiElement as the scope of an incident, LintDriver will use a
// PSI-specific code path to deduce suppressions. We need to test this path
// explicitly, so tests may choose to look for this issue.
// See LintDriver.isSuppressedLocally,
// and LintDriver.isSuppressed(context: JavaContext?, issue: Issue, scope:
// PsiElement?)
context.report(PSI_TEST_ISSUE, scope = node.sourcePsi, location, message)
} else if (s == "testAccessLibraryResource") {
// Trigger scenario in testAccessLibraryResource()
val resources =
context.client.getResources(
context.project,
ResourceRepositoryScope.LOCAL_DEPENDENCIES,
)
resources.getResources(ResourceNamespace.RES_AUTO, ResourceType.STRING, "local")
val lib = resources.getResources(ResourceNamespace.RES_AUTO, ResourceType.STRING, "lib")
lib.first().source // Trigger error
} else if (s == "testAccessMainProject") {
context.mainProject
}
}
}
}
companion object {
val TEST_ISSUE =
Issue.create(
"_TestIssueId",
"Not applicable",
"Not applicable",
Category.MESSAGES,
5,
Severity.WARNING,
Implementation(NoLocationNodeDetector::class.java, Scope.JAVA_FILE_SCOPE),
)
val PSI_TEST_ISSUE =
Issue.create(
"_PsiTestIssueId",
"Not applicable",
"Not applicable",
Category.MESSAGES,
5,
Severity.WARNING,
Implementation(NoLocationNodeDetector::class.java, Scope.JAVA_FILE_SCOPE),
)
}
}