summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/tooling/lint
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:34:42 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:34:42 +0000
commitda4c7e7ed675c3bf405668739c3012d140856109 (patch)
treecdd868dba063fecba609a1d819de271f0d51b23e /mobile/android/android-components/components/tooling/lint
parentAdding upstream version 125.0.3. (diff)
downloadfirefox-da4c7e7ed675c3bf405668739c3012d140856109.tar.xz
firefox-da4c7e7ed675c3bf405668739c3012d140856109.zip
Adding upstream version 126.0.upstream/126.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/components/tooling/lint')
-rw-r--r--mobile/android/android-components/components/tooling/lint/README.md11
-rw-r--r--mobile/android/android-components/components/tooling/lint/build.gradle42
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/AndroidSrcXmlDetector.kt82
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ConceptFetchDetector.kt249
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/FactCollectDetector.kt103
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetector.kt81
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintIssueRegistry.kt25
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintLogChecks.kt61
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/NotificationManagerChecks.kt84
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetector.kt97
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/AndroidSrcXmlDetectorTest.kt68
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ConceptFetchDetectorTest.kt277
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/FactCollectDetectorTest.kt220
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetectorTest.kt69
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/LintLogChecksTest.kt56
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetectorTest.kt68
-rw-r--r--mobile/android/android-components/components/tooling/lint/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
17 files changed, 1595 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/tooling/lint/README.md b/mobile/android/android-components/components/tooling/lint/README.md
new file mode 100644
index 0000000000..55a7f7fe91
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/README.md
@@ -0,0 +1,11 @@
+# [Android Components](../../../README.md) > Tooling > Lint
+
+Custom Lint rules for the components repository.
+
+These additional lint rules are run as part of the _Android Components_ build pipeline. Currently we do not publish packaged versions of these lint rules for consumption outside of the _Android Components_ repository.
+
+## License
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/
diff --git a/mobile/android/android-components/components/tooling/lint/build.gradle b/mobile/android/android-components/components/tooling/lint/build.gradle
new file mode 100644
index 0000000000..adee887580
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/build.gradle
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+apply plugin: 'java-library'
+apply plugin: 'kotlin'
+
+dependencies {
+ compileOnly ComponentsDependencies.tools_lintapi
+ compileOnly ComponentsDependencies.tools_lintchecks
+
+ compileOnly ComponentsDependencies.kotlin_reflect
+ testImplementation ComponentsDependencies.kotlin_reflect
+
+ testImplementation ComponentsDependencies.tools_lint
+ testImplementation ComponentsDependencies.tools_linttests
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_mockito
+}
+
+jar {
+ manifest {
+ attributes('Lint-Registry-v2': 'mozilla.components.tooling.lint.LintIssueRegistry')
+ }
+}
+
+tasks.register("lint") {
+ doLast {
+ // Do nothing. We execute the same set of tasks for all our modules in parallel on taskcluster.
+ // This project doesn't have a lint task. To avoid special casing our automation I just added
+ // an empty lint task here.
+ }
+}
+
+tasks.register("assembleAndroidTest") {
+ doLast {
+ // Do nothing. Like the `lint` task above this is just a dummy task so that this module
+ // behaves like our others and we do not need to special case it in automation.
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/AndroidSrcXmlDetector.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/AndroidSrcXmlDetector.kt
new file mode 100644
index 0000000000..5799871f4a
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/AndroidSrcXmlDetector.kt
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.tooling.lint
+
+import com.android.SdkConstants.ATTR_SRC
+import com.android.SdkConstants.FQCN_IMAGE_BUTTON
+import com.android.SdkConstants.FQCN_IMAGE_VIEW
+import com.android.SdkConstants.IMAGE_BUTTON
+import com.android.SdkConstants.IMAGE_VIEW
+import com.android.resources.ResourceFolderType
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.ResourceXmlDetector
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.XmlContext
+import com.google.common.annotations.VisibleForTesting
+import org.w3c.dom.Element
+
+/**
+ * A custom lint check that prohibits not using the app:srcCompat for ImageViews
+ */
+class AndroidSrcXmlDetector : ResourceXmlDetector() {
+ companion object {
+ const val SCHEMA = "http://schemas.android.com/apk/res/android"
+ const val FULLY_QUALIFIED_APP_COMPAT_IMAGE_BUTTON =
+ "androidx.appcompat.widget.AppCompatImageButton"
+ const val FULLY_QUALIFIED_APP_COMPAT_VIEW_CLASS =
+ "androidx.appcompat.widget.AppCompatImageView"
+ const val APP_COMPAT_IMAGE_BUTTON = "AppCompatImageButton"
+ const val APP_COMPAT_IMAGE_VIEW = "AppCompatImageView"
+
+ const val ERROR_MESSAGE = "Using android:src to define resource instead of app:srcCompat"
+
+ @VisibleForTesting
+ val ISSUE_XML_SRC_USAGE = Issue.create(
+ id = "AndroidSrcXmlDetector",
+ briefDescription = "Prohibits using android:src in ImageViews and ImageButtons",
+ explanation = "ImageView (and descendants) images should be declared using app:srcCompat",
+ category = Category.CORRECTNESS,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ AndroidSrcXmlDetector::class.java,
+ Scope.RESOURCE_FILE_SCOPE,
+ ),
+ )
+ }
+
+ override fun appliesTo(folderType: ResourceFolderType): Boolean {
+ // Return true if we want to analyze resource files in the specified resource
+ // folder type. In this case we only need to analyze layout resource files.
+ return folderType == ResourceFolderType.LAYOUT
+ }
+
+ override fun getApplicableElements(): Collection<String>? {
+ return setOf(
+ FQCN_IMAGE_VIEW,
+ IMAGE_VIEW,
+ FQCN_IMAGE_BUTTON,
+ IMAGE_BUTTON,
+ FULLY_QUALIFIED_APP_COMPAT_IMAGE_BUTTON,
+ FULLY_QUALIFIED_APP_COMPAT_VIEW_CLASS,
+ APP_COMPAT_IMAGE_BUTTON,
+ APP_COMPAT_IMAGE_VIEW,
+ )
+ }
+
+ override fun visitElement(context: XmlContext, element: Element) {
+ if (!element.hasAttributeNS(SCHEMA, ATTR_SRC)) return
+ val node = element.getAttributeNodeNS(SCHEMA, ATTR_SRC)
+
+ context.report(
+ issue = ISSUE_XML_SRC_USAGE,
+ scope = node,
+ location = context.getLocation(node),
+ message = ERROR_MESSAGE,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ConceptFetchDetector.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ConceptFetchDetector.kt
new file mode 100644
index 0000000000..0a31e61620
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ConceptFetchDetector.kt
@@ -0,0 +1,249 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.tooling.lint
+
+import com.android.tools.lint.checks.CheckResultDetector
+import com.android.tools.lint.checks.DataFlowAnalyzer
+import com.android.tools.lint.checks.EscapeCheckingDataFlowAnalyzer
+import com.android.tools.lint.checks.TargetMethodDataFlowAnalyzer
+import com.android.tools.lint.checks.isMissingTarget
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.getUMethod
+import com.android.tools.lint.detector.api.isJava
+import com.android.tools.lint.detector.api.skipLabeledExpression
+import com.intellij.psi.LambdaUtil
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiResourceVariable
+import com.intellij.psi.PsiVariable
+import com.intellij.psi.util.PsiTreeUtil
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UCallableReferenceExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UQualifiedReferenceExpression
+import org.jetbrains.uast.getParentOfType
+
+/**
+ * Checks for missing [mozilla.components.concept.fetch.Response.close] call on fetches that might not have used the
+ * resources.
+ *
+ * Review the unit tests for examples on what this [Detector] can identify.
+ */
+class ConceptFetchDetector : Detector(), SourceCodeScanner {
+ override fun getApplicableMethodNames(): List<String> {
+ return listOf("fetch")
+ }
+
+ override fun visitMethodCall(
+ context: JavaContext,
+ node: UCallExpression,
+ method: PsiMethod,
+ ) {
+ val containingClass = method.containingClass ?: return
+ val evaluator = context.evaluator
+
+ if (evaluator.extendsClass(
+ containingClass,
+ CLIENT_CLS,
+ false,
+ )
+ ) {
+ val returnType = method.getUMethod()?.returnTypeReference ?: return
+ val qualifiedName = returnType.getQualifiedName()
+ if (qualifiedName != null && qualifiedName == RESPONSE_CLS) {
+ checkClosed(context, node)
+ }
+ }
+ }
+
+ @Suppress("ReturnCount") // Extracted from `CleanupDetector#checkRecycled`.
+ private fun checkClosed(context: JavaContext, node: UCallExpression) {
+ // If it's an AutoCloseable in a try-with-resources clause, don't flag it: these will be
+ // cleaned up automatically
+ if (node.sourcePsi.isTryWithResources()) {
+ return
+ }
+
+ val parentMethod = node.getParentOfType(UMethod::class.java) ?: return
+
+ // Check if any of the 'body' methods are used. They are all closeable; do not report.
+ val bodyMethodTracker = BodyMethodTracker(listOf(node))
+ if (parentMethod.wasMethodCalled(bodyMethodTracker)) {
+ return
+ }
+
+ // Check if response has escaped (particularly through an extension function); do not report.
+ val responseEscapedTracker = ResponseEscapedTracker(listOf(node))
+ if (parentMethod.hasEscaped(responseEscapedTracker)) {
+ return
+ }
+
+ // Check if 'use' or 'close' were called; do not report.
+ val closeableTracker = CloseableTracker(listOf(node), context)
+ if (!parentMethod.isMissingTarget(closeableTracker)) {
+ return
+ }
+
+ context.report(
+ ISSUE_FETCH_RESPONSE_CLOSE,
+ node,
+ context.getCallLocation(node, includeReceiver = true, includeArguments = false),
+ "Response created but not closed: did you forget to call `close()`?",
+ if (CheckResultDetector.isExpressionValueUnused(node)) {
+ fix()
+ .replace()
+ .name("Call close()")
+ .range(context.getLocation(node))
+ .end()
+ .with(".close()")
+ .build()
+ } else {
+ null
+ },
+ )
+ }
+
+ private fun UMethod.hasEscaped(analyzer: EscapeCheckingDataFlowAnalyzer): Boolean {
+ accept(analyzer)
+ return analyzer.escaped
+ }
+
+ private fun UMethod.wasMethodCalled(analyzer: BodyMethodTracker): Boolean {
+ accept(analyzer)
+ return analyzer.found
+ }
+
+ private fun PsiElement?.isTryWithResources(): Boolean {
+ return this != null &&
+ isJava(this) &&
+ PsiTreeUtil.getParentOfType(this, PsiResourceVariable::class.java) != null
+ }
+
+ private class BodyMethodTracker(
+ initial: Collection<UElement>,
+ initialReferences: Collection<PsiVariable> = emptyList(),
+ ) : DataFlowAnalyzer(initial, initialReferences) {
+ var found = false
+ override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression): Boolean {
+ val methodName: String? = with(node.selector as? UCallExpression) {
+ this?.methodName ?: this?.methodIdentifier?.name
+ }
+
+ when (methodName) {
+ USE_STREAM,
+ USE_BUFFERED_READER,
+ STRING,
+ -> {
+ if (node.receiver.getExpressionType()?.canonicalText == BODY_CLS) {
+ // We are using any of the `body` methods which are all closeable.
+ found = true
+ return true
+ }
+ }
+ }
+
+ return super.visitQualifiedReferenceExpression(node)
+ }
+ }
+
+ private class ResponseEscapedTracker(
+ initial: Collection<UElement>,
+ ) : EscapeCheckingDataFlowAnalyzer(initial, emptyList()) {
+ override fun returnsSelf(call: UCallExpression): Boolean {
+ val type = call.receiver?.getExpressionType()?.canonicalText ?: return super.returnsSelf(call)
+ return type == RESPONSE_CLS
+ }
+ }
+
+ // Extracted from `CleanupDetector#checkRecycled#visitor`.
+ private class CloseableTracker(
+ initial: Collection<UElement>,
+ private val context: JavaContext,
+ ) : TargetMethodDataFlowAnalyzer(initial, emptyList()) {
+ override fun isTargetMethodName(name: String): Boolean {
+ return name == USE || name == CLOSE
+ }
+
+ @Suppress("ReturnCount")
+ override fun isTargetMethod(
+ name: String,
+ method: PsiMethod?,
+ call: UCallExpression?,
+ methodRef: UCallableReferenceExpression?,
+ ): Boolean {
+ if (USE == name) {
+ // Kotlin: "use" calls close;
+ // Ensure that "use" call accepts a single lambda parameter, so that it would
+ // loosely match kotlin.io.use() signature and at the same time allow custom
+ // overloads for types not extending Closeable
+ if (call != null && call.valueArgumentCount == 1) {
+ val argumentType =
+ call.valueArguments.first().skipLabeledExpression().getExpressionType()
+ if (argumentType != null && LambdaUtil.isFunctionalType(argumentType)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ if (method != null) {
+ val containingClass = method.containingClass
+ val targetName = containingClass?.qualifiedName ?: return true
+ if (targetName == RESPONSE_CLS) {
+ return true
+ }
+ val recycleClass =
+ context.evaluator.findClass(RESPONSE_CLS) ?: return true
+ return context.evaluator.extendsClass(recycleClass, targetName, false)
+ } else {
+ // Unresolved method call -- assume it's okay
+ return true
+ }
+ }
+ }
+
+ companion object {
+ @JvmField
+ val ISSUE_FETCH_RESPONSE_CLOSE = Issue.create(
+ id = "FetchResponseClose",
+ briefDescription = "Response stream fetched but not closed.",
+ explanation = """
+ A `Client.fetch` returns a `Response` that, on success, is consumed typically with
+ a `use` stream in Kotlin or a try-with-resources in Java. In the failure or manual
+ resource managed cases, we need to ensure that `Response.close` is always called.
+
+ Additionally, all methods on `Response.body` are AutoCloseable so using any of
+ those will release those resources after execution.
+ """.trimIndent(),
+ category = Category.CORRECTNESS,
+ priority = 6,
+ severity = Severity.ERROR,
+ androidSpecific = true,
+ implementation = Implementation(
+ ConceptFetchDetector::class.java,
+ Scope.JAVA_FILE_SCOPE,
+ ),
+ )
+
+ // Target method names
+ private const val CLOSE = "close"
+ private const val USE = "use"
+ private const val USE_STREAM = "useStream"
+ private const val USE_BUFFERED_READER = "useBufferedReader"
+ private const val STRING = "string"
+
+ private const val CLIENT_CLS = "mozilla.components.concept.fetch.Client"
+ private const val RESPONSE_CLS = "mozilla.components.concept.fetch.Response"
+ private const val BODY_CLS = "mozilla.components.concept.fetch.Response.Body"
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/FactCollectDetector.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/FactCollectDetector.kt
new file mode 100644
index 0000000000..4cd3f32f97
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/FactCollectDetector.kt
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.tooling.lint
+
+import com.android.tools.lint.checks.DataFlowAnalyzer
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UReturnExpression
+import org.jetbrains.uast.getParentOfType
+
+/**
+ * A custom lint check that warns if [Fact.collect()] is not called on a newly created [Fact] instance
+ */
+class FactCollectDetector : Detector(), SourceCodeScanner {
+
+ companion object {
+ private const val FULLY_QUALIFIED_FACT_CLASS_NAME =
+ "mozilla.components.support.base.facts.Fact"
+ private const val EXPECTED_METHOD_SIMPLE_NAME =
+ "collect" // The `Fact.collect` extension method
+
+ private val IMPLEMENTATION = Implementation(
+ FactCollectDetector::class.java,
+ Scope.JAVA_FILE_SCOPE,
+ )
+
+ val ISSUE_FACT_COLLECT_CALLED: Issue = Issue
+ .create(
+ id = "FactCollect",
+ briefDescription = "Fact created but not collected",
+ explanation = """
+ An instance of `Fact` was created but not collected. You must call
+ `collect()` on the instance to actually process it.
+ """.trimIndent(),
+ category = Category.CORRECTNESS,
+ priority = 6,
+ severity = Severity.ERROR,
+ implementation = IMPLEMENTATION,
+ )
+ }
+
+ override fun getApplicableConstructorTypes(): List<String> {
+ return listOf(FULLY_QUALIFIED_FACT_CLASS_NAME)
+ }
+
+ override fun visitConstructor(
+ context: JavaContext,
+ node: UCallExpression,
+ constructor: PsiMethod,
+ ) {
+ var isCollectCalled = false
+ var escapes = false
+ val visitor = object : DataFlowAnalyzer(setOf(node)) {
+ override fun receiver(call: UCallExpression) {
+ if (call.methodName == EXPECTED_METHOD_SIMPLE_NAME) {
+ isCollectCalled = true
+ }
+ }
+
+ override fun argument(call: UCallExpression, reference: UElement) {
+ escapes = true
+ }
+
+ override fun field(field: UElement) {
+ escapes = true
+ }
+
+ override fun returns(expression: UReturnExpression) {
+ escapes = true
+ }
+ }
+ val method = node.getParentOfType<UMethod>(UMethod::class.java, true) ?: return
+ method.accept(visitor)
+ if (!isCollectCalled && !escapes) {
+ reportUsage(context, node)
+ }
+ }
+
+ private fun reportUsage(context: JavaContext, node: UCallExpression) {
+ context.report(
+ issue = ISSUE_FACT_COLLECT_CALLED,
+ scope = node,
+ location = context.getCallLocation(
+ call = node,
+ includeReceiver = true,
+ includeArguments = false,
+ ),
+ message = "Fact created but not shown: did you forget to call `collect()` ?",
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetector.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetector.kt
new file mode 100644
index 0000000000..f775f6ee3f
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetector.kt
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.tooling.lint
+
+import com.android.SdkConstants.ATTR_TINT
+import com.android.SdkConstants.FQCN_IMAGE_BUTTON
+import com.android.SdkConstants.FQCN_IMAGE_VIEW
+import com.android.SdkConstants.IMAGE_BUTTON
+import com.android.SdkConstants.IMAGE_VIEW
+import com.android.resources.ResourceFolderType
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.ResourceXmlDetector
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.XmlContext
+import org.w3c.dom.Element
+
+/**
+ * A custom lint check that prohibits not using the app:tint for ImageViews
+ */
+class ImageViewAndroidTintXmlDetector : ResourceXmlDetector() {
+ companion object {
+ const val SCHEMA = "http://schemas.android.com/apk/res/android"
+ const val FULLY_QUALIFIED_APP_COMPAT_IMAGE_BUTTON =
+ "androidx.appcompat.widget.AppCompatImageButton"
+ const val FULLY_QUALIFIED_APP_COMPAT_VIEW_CLASS =
+ "androidx.appcompat.widget.AppCompatImageView"
+ const val APP_COMPAT_IMAGE_BUTTON = "AppCompatImageButton"
+ const val APP_COMPAT_IMAGE_VIEW = "AppCompatImageView"
+
+ const val ERROR_MESSAGE =
+ "Using android:tint to tint ImageView instead of app:tint with AppCompatImageView"
+
+ val ISSUE_XML_SRC_USAGE = Issue.create(
+ id = "AndroidSrcXmlDetector",
+ briefDescription = "Prohibits using android:tint in ImageViews and ImageButtons",
+ explanation = "ImageView (and descendants) should be tinted using app:tint",
+ category = Category.CORRECTNESS,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ ImageViewAndroidTintXmlDetector::class.java,
+ Scope.RESOURCE_FILE_SCOPE,
+ ),
+ )
+ }
+
+ override fun appliesTo(folderType: ResourceFolderType): Boolean {
+ // Return true if we want to analyze resource files in the specified resource
+ // folder type. In this case we only need to analyze layout resource files.
+ return folderType == ResourceFolderType.LAYOUT
+ }
+
+ override fun getApplicableElements(): Collection<String>? {
+ return setOf(
+ FQCN_IMAGE_VIEW,
+ IMAGE_VIEW,
+ FQCN_IMAGE_BUTTON,
+ IMAGE_BUTTON,
+ FULLY_QUALIFIED_APP_COMPAT_IMAGE_BUTTON,
+ FULLY_QUALIFIED_APP_COMPAT_VIEW_CLASS,
+ APP_COMPAT_IMAGE_BUTTON,
+ APP_COMPAT_IMAGE_VIEW,
+ )
+ }
+
+ override fun visitElement(context: XmlContext, element: Element) {
+ if (!element.hasAttributeNS(SCHEMA, ATTR_TINT)) return
+ val node = element.getAttributeNodeNS(SCHEMA, ATTR_TINT)
+
+ context.report(
+ issue = ISSUE_XML_SRC_USAGE,
+ scope = node,
+ location = context.getLocation(node),
+ message = ERROR_MESSAGE,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintIssueRegistry.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintIssueRegistry.kt
new file mode 100644
index 0000000000..31118405dc
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintIssueRegistry.kt
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.tooling.lint
+
+import com.android.tools.lint.client.api.IssueRegistry
+import com.android.tools.lint.detector.api.Issue
+
+/**
+ * Registry which provides a list of our custom lint checks to be performed on an Android project.
+ */
+@Suppress("unused")
+class LintIssueRegistry : IssueRegistry() {
+ override val api: Int = com.android.tools.lint.detector.api.CURRENT_API
+ override val issues: List<Issue> = listOf(
+ LintLogChecks.ISSUE_LOG_USAGE,
+ AndroidSrcXmlDetector.ISSUE_XML_SRC_USAGE,
+ TextViewAndroidSrcXmlDetector.ISSUE_XML_SRC_USAGE,
+ ImageViewAndroidTintXmlDetector.ISSUE_XML_SRC_USAGE,
+ FactCollectDetector.ISSUE_FACT_COLLECT_CALLED,
+ NotificationManagerChecks.ISSUE_NOTIFICATION_USAGE,
+ ConceptFetchDetector.ISSUE_FETCH_RESPONSE_CLOSE,
+ )
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintLogChecks.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintLogChecks.kt
new file mode 100644
index 0000000000..a462335162
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/LintLogChecks.kt
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.tooling.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.getContainingUClass
+import java.util.EnumSet
+
+internal const val ANDROID_LOG_CLASS = "android.util.Log"
+internal const val ERROR_MESSAGE = "Using Android Log instead of base component"
+
+/**
+ * Custom lint checks related to logging.
+ */
+class LintLogChecks : Detector(), Detector.UastScanner {
+ private val componentPackages = listOf("mozilla.components", "org.mozilla.telemetry", "org.mozilla.samples")
+
+ override fun getApplicableMethodNames() = listOf("v", "d", "i", "w", "e")
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ if (context.evaluator.isMemberInClass(method, ANDROID_LOG_CLASS)) {
+ val inComponentPackage = componentPackages.any {
+ node.methodIdentifier?.getContainingUClass()?.qualifiedName?.startsWith(it) == true
+ }
+
+ if (inComponentPackage) {
+ context.report(
+ ISSUE_LOG_USAGE,
+ node,
+ context.getLocation(node),
+ ERROR_MESSAGE,
+ )
+ }
+ }
+ }
+
+ companion object {
+ internal val ISSUE_LOG_USAGE = Issue.create(
+ "LogUsage",
+ "Log/Logger from base component should be used.",
+ """The Log or Logger class from the base component should be used for logging instead of
+ Android's Log class. This will allow the app to control what logs should be accepted
+ and how they should be processed.
+ """.trimIndent(),
+ Category.MESSAGES,
+ 5,
+ Severity.WARNING,
+ Implementation(LintLogChecks::class.java, EnumSet.of(Scope.JAVA_FILE)),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/NotificationManagerChecks.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/NotificationManagerChecks.kt
new file mode 100644
index 0000000000..b2141abbbb
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/NotificationManagerChecks.kt
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.tooling.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.getContainingUClass
+import java.util.EnumSet
+
+internal const val ANDROID_NOTIFICATION_MANAGER_COMPAT_CLASS =
+ "androidx.core.app.NotificationManagerCompat"
+internal const val ANDROID_NOTIFICATION_MANAGER_CLASS =
+ "android.app.NotificationManager"
+
+internal const val NOTIFY_ERROR_MESSAGE = "Using Android NOTIFY instead of base component"
+
+/**
+ * Custom lint that ensures [NotificationManagerCompat] and [NotificationManager]'s method [notify]
+ * is not called directly from code.
+ * Calling notify directly from code eludes the checks implemented in [NotificationsDelegate]
+ */
+class NotificationManagerChecks : Detector(), Detector.UastScanner {
+ private val componentPackages =
+ listOf("mozilla.components", "org.mozilla.telemetry", "org.mozilla.samples")
+ private val appPackages = listOf("org.mozilla.fenix", "org.mozilla.focus")
+
+ override fun getApplicableMethodNames() = listOf("notify")
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ if (context.evaluator.isMemberInClass(method, ANDROID_NOTIFICATION_MANAGER_COMPAT_CLASS) ||
+ context.evaluator.isMemberInClass(method, ANDROID_NOTIFICATION_MANAGER_CLASS)
+ ) {
+ val inComponentPackage = componentPackages.any {
+ node.methodIdentifier?.getContainingUClass()?.qualifiedName?.startsWith(it) == true
+ }
+
+ val inAppPackage = appPackages.any {
+ node.methodIdentifier?.getContainingUClass()?.qualifiedName?.startsWith(it) == true
+ }
+
+ if (inComponentPackage) {
+ context.report(
+ ISSUE_NOTIFICATION_USAGE,
+ node,
+ context.getLocation(node),
+ NOTIFY_ERROR_MESSAGE,
+ )
+ }
+
+ if (inAppPackage) {
+ context.report(
+ ISSUE_NOTIFICATION_USAGE,
+ node,
+ context.getLocation(node),
+ NOTIFY_ERROR_MESSAGE,
+ )
+ }
+ }
+ }
+
+ companion object {
+ internal val ISSUE_NOTIFICATION_USAGE = Issue.create(
+ "NotifyUsage",
+ "NotificationsDelegate should be used instead of NotificationManager.",
+ """NotificationsDelegate should be used for showing notifications instead of a NotificationManager
+ or a NotificationManagerCompat. This will allow the app to control requesting the notification permission
+ when needed and handling the request result.
+ """.trimIndent(),
+ Category.MESSAGES,
+ 5,
+ Severity.WARNING,
+ Implementation(NotificationManagerChecks::class.java, EnumSet.of(Scope.JAVA_FILE)),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetector.kt b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetector.kt
new file mode 100644
index 0000000000..0be9ab89a8
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/main/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetector.kt
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.tooling.lint
+
+import com.android.SdkConstants.ATTR_DRAWABLE_BOTTOM
+import com.android.SdkConstants.ATTR_DRAWABLE_END
+import com.android.SdkConstants.ATTR_DRAWABLE_LEFT
+import com.android.SdkConstants.ATTR_DRAWABLE_RIGHT
+import com.android.SdkConstants.ATTR_DRAWABLE_START
+import com.android.SdkConstants.ATTR_DRAWABLE_TOP
+import com.android.SdkConstants.FQCN_TEXT_VIEW
+import com.android.SdkConstants.TEXT_VIEW
+import com.android.resources.ResourceFolderType
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.ResourceXmlDetector
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.XmlContext
+import org.w3c.dom.Element
+
+/**
+ * A custom lint check that prohibits not using the app:srcCompat for ImageViews
+ */
+class TextViewAndroidSrcXmlDetector : ResourceXmlDetector() {
+ companion object {
+ const val SCHEMA = "http://schemas.android.com/apk/res/android"
+
+ const val ERROR_MESSAGE =
+ "Using android:drawableX to define resource instead of app:drawableXCompat"
+
+ val ISSUE_XML_SRC_USAGE = Issue.create(
+ id = "TextViewAndroidSrcXmlDetector",
+ briefDescription = "Prohibits using android namespace to define drawables in TextViews",
+ explanation = "TextView drawables should be declared using app:drawableXCompat",
+ category = Category.CORRECTNESS,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ TextViewAndroidSrcXmlDetector::class.java,
+ Scope.RESOURCE_FILE_SCOPE,
+ ),
+ )
+ }
+
+ override fun appliesTo(folderType: ResourceFolderType): Boolean {
+ // Return true if we want to analyze resource files in the specified resource
+ // folder type. In this case we only need to analyze layout resource files.
+ return folderType == ResourceFolderType.LAYOUT
+ }
+
+ override fun getApplicableElements(): Collection<String>? {
+ return setOf(
+ FQCN_TEXT_VIEW,
+ TEXT_VIEW,
+ )
+ }
+
+ override fun visitElement(context: XmlContext, element: Element) {
+ val node = when {
+ element.hasAttributeNS(SCHEMA, ATTR_DRAWABLE_BOTTOM) -> element.getAttributeNodeNS(
+ SCHEMA,
+ ATTR_DRAWABLE_BOTTOM,
+ )
+ element.hasAttributeNS(SCHEMA, ATTR_DRAWABLE_END) -> element.getAttributeNodeNS(
+ SCHEMA,
+ ATTR_DRAWABLE_END,
+ )
+ element.hasAttributeNS(SCHEMA, ATTR_DRAWABLE_LEFT) -> element.getAttributeNodeNS(
+ SCHEMA,
+ ATTR_DRAWABLE_LEFT,
+ )
+ element.hasAttributeNS(
+ SCHEMA,
+ ATTR_DRAWABLE_RIGHT,
+ ) -> element.getAttributeNodeNS(SCHEMA, ATTR_DRAWABLE_RIGHT)
+ element.hasAttributeNS(
+ SCHEMA,
+ ATTR_DRAWABLE_START,
+ ) -> element.getAttributeNodeNS(SCHEMA, ATTR_DRAWABLE_START)
+ element.hasAttributeNS(SCHEMA, ATTR_DRAWABLE_TOP) -> element.getAttributeNodeNS(
+ SCHEMA,
+ ATTR_DRAWABLE_TOP,
+ )
+ else -> null
+ } ?: return
+
+ context.report(
+ issue = ISSUE_XML_SRC_USAGE,
+ scope = node,
+ location = context.getLocation(node),
+ message = ERROR_MESSAGE,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/AndroidSrcXmlDetectorTest.kt b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/AndroidSrcXmlDetectorTest.kt
new file mode 100644
index 0000000000..bae0412d45
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/AndroidSrcXmlDetectorTest.kt
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.tooling.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ * Tests for the [AndroidSrcXmlDetector] custom lint check.
+ */
+@RunWith(JUnit4::class)
+class AndroidSrcXmlDetectorTest : LintDetectorTest() {
+
+ override fun getIssues(): MutableList<Issue> =
+ mutableListOf(AndroidSrcXmlDetector.ISSUE_XML_SRC_USAGE)
+
+ override fun getDetector(): Detector = AndroidSrcXmlDetector()
+
+ @Test
+ fun expectPass() {
+ lint()
+ .files(
+ xml(
+ "res/layout/layout.xml",
+ """
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ />
+""",
+ ),
+ ).allowMissingSdk(true)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun expectFail() {
+ lint()
+ .files(
+ xml(
+ "res/layout/layout.xml",
+ """
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/ic_close"
+ />
+""",
+ ),
+ ).allowMissingSdk(true)
+ .run()
+ .expect(
+ """
+res/layout/layout.xml:5: Error: Using android:src to define resource instead of app:srcCompat [AndroidSrcXmlDetector]
+ android:src="@drawable/ic_close"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+ """,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ConceptFetchDetectorTest.kt b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ConceptFetchDetectorTest.kt
new file mode 100644
index 0000000000..cd60356015
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ConceptFetchDetectorTest.kt
@@ -0,0 +1,277 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.tooling.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFiles.gradle
+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
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ConceptFetchDetectorTest {
+
+ @Test
+ fun `should report when close is not invoked on a Response instance`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.concept.fetch.*
+
+ val client = Client()
+
+ fun isSuccessful() : Boolean {
+ val response = client.fetch(Request("https://mozilla.org"))
+ return response.isSuccess
+ }
+ """.trimIndent(),
+ ),
+ responseClassfileStub,
+ clientClassFileStub,
+ )
+ .issues(ConceptFetchDetector.ISSUE_FETCH_RESPONSE_CLOSE)
+ .run()
+ .expect(
+ """
+ src/test/test.kt:8: Error: Response created but not closed: did you forget to call close()? [FetchResponseClose]
+ val response = client.fetch(Request("https://mozilla.org"))
+ ~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.trimIndent(),
+ )
+ }
+
+ @Test
+ fun `should not report from a result that is closed in another function`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.concept.fetch.*
+
+ val client = Client()
+
+ fun getResult() {
+ return try {
+ client.fetch(request).toResult()
+ } catch (e: IOException) {
+ Logger.debug(message = "Could not fetch region from location service", throwable = e)
+ null
+ }
+ }
+
+ data class Result(
+ val name: String,
+ )
+
+ private fun Response.toResult(): Region? {
+ if (!this.isSuccess) {
+ close()
+ return null
+ }
+
+ use {
+ return try {
+ Result("{}")
+ } catch (e: JSONException) {
+ Logger.debug(message = "Could not parse JSON returned from service", throwable = e)
+ null
+ }
+ }
+ }
+ """.trimIndent(),
+ ),
+ responseClassfileStub,
+ clientClassFileStub,
+ )
+ .issues(ConceptFetchDetector.ISSUE_FETCH_RESPONSE_CLOSE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `should pass when auto-closeable 'use' function is invoked`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.concept.fetch.*
+ import kotlin.io.*
+
+ val client = Client()
+
+ fun getResult() {
+ client.fetch(request).use { response ->
+ response.hashCode()
+ }
+ }
+ """.trimIndent(),
+ ),
+ responseClassfileStub,
+ clientClassFileStub,
+ )
+ .issues(ConceptFetchDetector.ISSUE_FETCH_RESPONSE_CLOSE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `should pass if body (auto-closeable methods) is used from the response`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.concept.fetch.*
+ import kotlin.io.*
+
+ val client = Client()
+
+ fun getResult() { // OK
+ val response = client.fetch(request)
+ response?.body.string(Charset.UTF_8)
+ }
+
+ fun getResult2() { // OK
+ client.fetch(request).body.useStream()
+ }
+
+ fun getResult3() { // OK; escaped.
+ val response = client.fetch(request)
+ process(response)
+ }
+
+ fun process(response: Response) {
+ response.hashCode()
+ }
+ """.trimIndent(),
+ ),
+ responseClassfileStub,
+ clientClassFileStub,
+ )
+ .issues(ConceptFetchDetector.ISSUE_FETCH_RESPONSE_CLOSE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `should pass if try-with-resources is used from the response`() {
+ lint()
+ .files(
+ gradle(
+ // For `try (cursor)` (without declaration) we'll need level 9
+ // or PSI/UAST will return an empty variable list
+ """
+ android {
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_9
+ targetCompatibility JavaVersion.VERSION_1_9
+ }
+ }
+ """,
+ ).indented(),
+ java(
+ """
+ package test;
+
+ import mozilla.components.concept.fetch.Client;
+ import mozilla.components.concept.fetch.Response;
+ import mozilla.components.concept.fetch.Response.Body;
+ import mozilla.components.concept.fetch.Request;
+
+ public class TryWithResources {
+ public void test(Client client, Request request) {
+ try(Response response = client.fetch(request)) {
+ if (response != null) {
+ //noinspection StatementWithEmptyBody
+ while (response.hashCode()) {
+ // ..
+ }
+ }
+ } catch (Exception e) {
+ // do nothing
+ }
+ }
+ }
+ """.trimIndent(),
+ ),
+ responseClassfileStub,
+ clientClassFileStub,
+ )
+ .issues(ConceptFetchDetector.ISSUE_FETCH_RESPONSE_CLOSE)
+ .run()
+ .expectClean()
+ }
+
+ private val clientClassFileStub = kotlin(
+ """
+ package mozilla.components.concept.fetch
+
+ data class Request(
+ val url: String,
+ val method: Method = Method.GET,
+ val headers: MutableHeaders? = MutableHeaders(),
+ val connectTimeout: Pair<Long, TimeUnit>? = null,
+ val readTimeout: Pair<Long, TimeUnit>? = null,
+ val body: Body? = null,
+ val redirect: Redirect = Redirect.FOLLOW,
+ val cookiePolicy: CookiePolicy = CookiePolicy.INCLUDE,
+ val useCaches: Boolean = true,
+ val private: Boolean = false,
+ )
+
+ class Client {
+ fun fetch(request: Request): Response {
+ return Response(
+ url = "https://mozilla.org",
+ )
+ }
+ }
+ """.trimIndent(),
+ )
+ private val responseClassfileStub = kotlin(
+ """
+ package mozilla.components.concept.fetch
+
+ data class Response(
+ val url: String,
+ val status: Int,
+ val headers: Headers,
+ val body: Body,
+ ) : Closeable {
+ override fun close() {
+ body.close()
+ }
+
+ open class Body(
+ private val stream: InputStream,
+ contentType: String? = null,
+ ) {
+ fun <R> useStream(block: (InputStream) -> R): R {
+ }
+
+ fun <R> useBufferedReader(charset: Charset? = null, block: (BufferedReader) -> R): R = use {
+ }
+
+ fun string(charset: Charset? = null): String = use {
+ }
+ }
+ }
+
+ val Response.isSuccess: Boolean
+ get() = status in SUCCESS_STATUS_RANGE
+ """,
+ ).indented()
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/FactCollectDetectorTest.kt b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/FactCollectDetectorTest.kt
new file mode 100644
index 0000000000..b70eca91c4
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/FactCollectDetectorTest.kt
@@ -0,0 +1,220 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.tooling.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
+import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ * Tests for the [FactCollectDetector] custom lint check.
+ */
+@RunWith(JUnit4::class)
+class FactCollectDetectorTest {
+
+ private val factClassfileStub = kotlin(
+ """
+ package mozilla.components.support.base.facts
+
+ data class Fact(
+ val component: Component,
+ val action: Action,
+ val item: String,
+ val value: String? = null,
+ val metadata: Map<String, Any>? = null
+ )
+
+ fun Fact.collect() = Facts.collect(this)
+ """,
+ ).indented()
+
+ @Test
+ fun `should report when collect is not invoked on Fact instance`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.support.base.facts.Fact
+ import mozilla.components.support.base.facts.collect
+
+ private fun emitAwesomebarFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null
+ ) {
+ Fact(
+ Component.BROWSER_AWESOMEBAR,
+ action,
+ item,
+ value,
+ metadata
+ )
+ }
+ """,
+ ).indented(),
+ factClassfileStub,
+ )
+ .issues(FactCollectDetector.ISSUE_FACT_COLLECT_CALLED)
+ .run()
+ .expect(
+ """
+ src/test/test.kt:12: Error: Fact created but not shown: did you forget to call collect() ? [FactCollect]
+ Fact(
+ ~~~~
+ 1 errors, 0 warnings
+ """.trimIndent(),
+ )
+ }
+
+ @Test
+ fun `should pass when collect is invoked on Fact instance`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.support.base.facts.Fact
+ import mozilla.components.support.base.facts.collect
+
+ private fun emitAwesomebarFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null
+ ) {
+ Fact(
+ Component.BROWSER_AWESOMEBAR,
+ action,
+ item,
+ value,
+ metadata
+ ).collect()
+ }
+ """,
+ ).indented(),
+ factClassfileStub,
+ )
+ .issues(FactCollectDetector.ISSUE_FACT_COLLECT_CALLED)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `should pass when an instance escapes through a return statement`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.support.base.facts.Fact
+ import mozilla.components.support.base.facts.collect
+
+ private fun createFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null
+ ): Fact {
+ return Fact(
+ Component.BROWSER_AWESOMEBAR,
+ action,
+ item,
+ value,
+ metadata
+ )
+ }
+ """,
+ ).indented(),
+ factClassfileStub,
+ )
+ .issues(FactCollectDetector.ISSUE_FACT_COLLECT_CALLED)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `should pass when an instance escapes through a method parameter`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.support.base.facts.Fact
+ import mozilla.components.support.base.facts.collect
+
+ private fun createFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null
+ ) {
+ val fact = Fact(
+ Component.BROWSER_AWESOMEBAR,
+ action,
+ item,
+ value,
+ metadata
+ )
+ method(fact)
+ }
+
+ private fun method(parameter: Fact) {
+
+ }
+ """,
+ ).indented(),
+ factClassfileStub,
+ )
+ .issues(FactCollectDetector.ISSUE_FACT_COLLECT_CALLED)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `should pass when an instance escapes through a field assignment`() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package test
+
+ import mozilla.components.support.base.facts.Fact
+ import mozilla.components.support.base.facts.collect
+
+ class FactSender {
+ private var fact: Fact? = null
+
+ private fun createFact(
+ action: Action,
+ item: String,
+ value: String? = null,
+ metadata: Map<String, Any>? = null
+ ) {
+ fact = Fact(
+ Component.BROWSER_AWESOMEBAR,
+ action,
+ item,
+ value,
+ metadata
+ )
+ }
+ }
+ """,
+ ).indented(),
+ factClassfileStub,
+ )
+ .issues(FactCollectDetector.ISSUE_FACT_COLLECT_CALLED)
+ .run()
+ .expectClean()
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetectorTest.kt b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetectorTest.kt
new file mode 100644
index 0000000000..0a715407bd
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/ImageViewAndroidTintXmlDetectorTest.kt
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.tooling.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ * Tests for the [ImageViewAndroidTintXmlDetector] custom lint check.
+ */
+@RunWith(JUnit4::class)
+class ImageViewAndroidTintXmlDetectorTest : LintDetectorTest() {
+
+ override fun getIssues(): MutableList<Issue> =
+ mutableListOf(ImageViewAndroidTintXmlDetector.ISSUE_XML_SRC_USAGE)
+
+ override fun getDetector(): Detector = ImageViewAndroidTintXmlDetector()
+
+ @Test
+ fun expectPass() {
+ lint()
+ .files(
+ xml(
+ "res/layout/layout.xml",
+ """
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ />
+""",
+ ),
+ ).allowMissingSdk(true)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun expectFail() {
+ lint()
+ .files(
+ xml(
+ "res/layout/layout.xml",
+ """
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/ic_close"
+ android:tint="@color/photonBlue90"
+ />
+""",
+ ),
+ ).allowMissingSdk(true)
+ .run()
+ .expect(
+ """
+res/layout/layout.xml:6: Error: Using android:tint to tint ImageView instead of app:tint with AppCompatImageView [AndroidSrcXmlDetector]
+ android:tint="@color/photonBlue90"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+ """,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/LintLogChecksTest.kt b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/LintLogChecksTest.kt
new file mode 100644
index 0000000000..1b10f883fa
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/LintLogChecksTest.kt
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.tooling.lint
+
+import com.android.tools.lint.client.api.JavaEvaluator
+import com.android.tools.lint.detector.api.JavaContext
+import com.intellij.psi.PsiMethod
+import mozilla.components.tooling.lint.LintLogChecks.Companion.ISSUE_LOG_USAGE
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UIdentifier
+import org.jetbrains.uast.getContainingUClass
+import org.junit.Test
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+
+class LintLogChecksTest {
+
+ @Test
+ fun `report log error in components code only`() {
+ val evaluator = mock(JavaEvaluator::class.java)
+ val context = mock(JavaContext::class.java)
+ val node = mock(UCallExpression::class.java)
+ val method = mock(PsiMethod::class.java)
+ val methodIdentifier = mock(UIdentifier::class.java)
+ val clazz = mock(UClass::class.java)
+
+ `when`(evaluator.isMemberInClass(method, ANDROID_LOG_CLASS)).thenReturn(true)
+ `when`(context.evaluator).thenReturn(evaluator)
+
+ val logCheck = LintLogChecks()
+ logCheck.visitMethodCall(context, node, method)
+ verify(context, never()).report(ISSUE_LOG_USAGE, node, context.getLocation(node), ERROR_MESSAGE)
+
+ `when`(node.methodIdentifier).thenReturn(methodIdentifier)
+ logCheck.visitMethodCall(context, node, method)
+ verify(context, never()).report(ISSUE_LOG_USAGE, node, context.getLocation(node), ERROR_MESSAGE)
+
+ `when`(methodIdentifier.getContainingUClass()).thenReturn(clazz)
+ logCheck.visitMethodCall(context, node, method)
+ verify(context, never()).report(ISSUE_LOG_USAGE, node, context.getLocation(node), ERROR_MESSAGE)
+
+ `when`(clazz.qualifiedName).thenReturn("com.some.app.Class")
+ logCheck.visitMethodCall(context, node, method)
+ verify(context, never()).report(ISSUE_LOG_USAGE, node, context.getLocation(node), ERROR_MESSAGE)
+
+ `when`(clazz.qualifiedName).thenReturn("mozilla.components.some.Class")
+ logCheck.visitMethodCall(context, node, method)
+ verify(context, times(1)).report(ISSUE_LOG_USAGE, node, context.getLocation(node), ERROR_MESSAGE)
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetectorTest.kt b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetectorTest.kt
new file mode 100644
index 0000000000..f764084350
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/test/java/mozilla/components/tooling/lint/TextViewAndroidSrcXmlDetectorTest.kt
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.tooling.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ * Tests for the [TextViewAndroidSrcXmlDetector] custom lint check.
+ */
+@RunWith(JUnit4::class)
+class TextViewAndroidSrcXmlDetectorTest : LintDetectorTest() {
+
+ override fun getIssues(): MutableList<Issue> =
+ mutableListOf(TextViewAndroidSrcXmlDetector.ISSUE_XML_SRC_USAGE)
+
+ override fun getDetector(): Detector = TextViewAndroidSrcXmlDetector()
+
+ @Test
+ fun expectPass() {
+ lint()
+ .files(
+ xml(
+ "res/layout/layout.xml",
+ """
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ />
+""",
+ ),
+ ).allowMissingSdk(true)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun expectFail() {
+ lint()
+ .files(
+ xml(
+ "res/layout/layout.xml",
+ """
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/ic_close"
+ />
+""",
+ ),
+ ).allowMissingSdk(true)
+ .run()
+ .expect(
+ """
+res/layout/layout.xml:5: Error: Using android:drawableX to define resource instead of app:drawableXCompat [TextViewAndroidSrcXmlDetector]
+ android:drawableStart="@drawable/ic_close"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+ """,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/tooling/lint/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/tooling/lint/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/tooling/lint/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,2 @@
+mock-maker-inline
+// This allows mocking final classes (classes are final by default in Kotlin)