diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
commit | da4c7e7ed675c3bf405668739c3012d140856109 (patch) | |
tree | cdd868dba063fecba609a1d819de271f0d51b23e /mobile/android/android-components/components/tooling/lint | |
parent | Adding upstream version 125.0.3. (diff) | |
download | firefox-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')
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) |