From 0bdba3938a6620abb2523782246279392742ac13 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Tue, 7 Mar 2023 13:42:44 -0500 Subject: [PATCH] Implement M2ApiDetector (#75) * Implement M2ApiDetector This implements an opt-in check that can be used to error against use of M2 APIs. This is intended to be useful for apps that have completed their M3 migrations or baseline existing M2 usages while completing their migrations. * Update docs/rules.md Co-authored-by: Chris Banes * Add setup instructions * Don't check imports since there's no file-level suppression * Add allow-list option * Doc cleanups * Implement M2ApiDetectorTest --------- Co-authored-by: Chris Banes --- .../lint/compose/ComposeLintsIssueRegistry.kt | 1 + .../java/slack/lint/compose/M2ApiDetector.kt | 83 ++++++++++++ .../slack/lint/compose/M2ApiDetectorTest.kt | 121 ++++++++++++++++++ docs/rules.md | 36 ++++++ 4 files changed, 241 insertions(+) create mode 100644 compose-lint-checks/src/main/java/slack/lint/compose/M2ApiDetector.kt create mode 100644 compose-lint-checks/src/test/java/slack/lint/compose/M2ApiDetectorTest.kt diff --git a/compose-lint-checks/src/main/java/slack/lint/compose/ComposeLintsIssueRegistry.kt b/compose-lint-checks/src/main/java/slack/lint/compose/ComposeLintsIssueRegistry.kt index c6bc126a..b95d3769 100644 --- a/compose-lint-checks/src/main/java/slack/lint/compose/ComposeLintsIssueRegistry.kt +++ b/compose-lint-checks/src/main/java/slack/lint/compose/ComposeLintsIssueRegistry.kt @@ -31,6 +31,7 @@ class ComposeLintsIssueRegistry : IssueRegistry() { ModifierMissingDetector.ISSUE, ModifierReusedDetector.ISSUE, ModifierWithoutDefaultDetector.ISSUE, + M2ApiDetector.ISSUE, MultipleContentEmittersDetector.ISSUE, MutableParametersDetector.ISSUE, ParameterOrderDetector.ISSUE, diff --git a/compose-lint-checks/src/main/java/slack/lint/compose/M2ApiDetector.kt b/compose-lint-checks/src/main/java/slack/lint/compose/M2ApiDetector.kt new file mode 100644 index 00000000..326b4747 --- /dev/null +++ b/compose-lint-checks/src/main/java/slack/lint/compose/M2ApiDetector.kt @@ -0,0 +1,83 @@ +// Copyright (C) 2022 Salesforce, Inc. +// SPDX-License-Identifier: Apache-2.0 +package slack.lint.compose + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Severity.IGNORE +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.android.tools.lint.detector.api.StringOption +import com.android.tools.lint.detector.api.TextFormat +import com.intellij.psi.PsiNamedElement +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UQualifiedReferenceExpression +import org.jetbrains.uast.UResolvable +import slack.lint.compose.util.OptionLoadingDetector +import slack.lint.compose.util.Priorities.NORMAL +import slack.lint.compose.util.StringSetLintOption +import slack.lint.compose.util.sourceImplementation + +internal class M2ApiDetector +@JvmOverloads +constructor(private val allowList: StringSetLintOption = StringSetLintOption(ALLOW_LIST)) : + OptionLoadingDetector(allowList), SourceCodeScanner { + + companion object { + private const val M2Package = "androidx.compose.material" + + internal val ALLOW_LIST = + StringOption( + "allowed-m2-apis", + "A comma-separated list of APIs in androidx.compose.material that should be allowed.", + null, + "This property should define a comma-separated list of APIs in androidx.compose.material that should be allowed." + ) + + val ISSUE = + Issue.create( + id = "ComposeM2Api", + briefDescription = "Using a Compose M2 API is not recommended", + explanation = + """ + Compose Material 2 (M2) is succeeded by Material 3 (M3). Please use M3 APIs. + See https://slackhq.github.io/compose-lints/rules/#use-material-3 for more information. + """, + category = CORRECTNESS, + priority = NORMAL, + severity = IGNORE, + implementation = sourceImplementation() + ) + .setOptions(listOf(ALLOW_LIST)) + } + + override fun getApplicableUastTypes() = + listOf>( + UCallExpression::class.java, + UQualifiedReferenceExpression::class.java, + ) + + override fun createUastHandler(context: JavaContext) = + object : UElementHandler() { + override fun visitCallExpression(node: UCallExpression) = checkNode(node) + + override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) = + checkNode(node) + + private fun checkNode(node: UResolvable) { + val resolved = node.resolve() ?: return + val packageName = context.evaluator.getPackage(resolved)?.qualifiedName ?: return + if (packageName == M2Package) { + // Ignore any in the allow-list. + if (resolved is PsiNamedElement && resolved.name in allowList.value) return + context.report( + issue = ISSUE, + location = context.getLocation(node), + message = ISSUE.getExplanation(TextFormat.TEXT), + ) + } + } + } +} diff --git a/compose-lint-checks/src/test/java/slack/lint/compose/M2ApiDetectorTest.kt b/compose-lint-checks/src/test/java/slack/lint/compose/M2ApiDetectorTest.kt new file mode 100644 index 00000000..2c261929 --- /dev/null +++ b/compose-lint-checks/src/test/java/slack/lint/compose/M2ApiDetectorTest.kt @@ -0,0 +1,121 @@ +// Copyright (C) 2023 Salesforce, Inc. +// SPDX-License-Identifier: Apache-2.0 +package slack.lint.compose + +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +class M2ApiDetectorTest : BaseSlackLintTest() { + + override fun getDetector(): Detector = M2ApiDetector() + override fun getIssues(): List = listOf(M2ApiDetector.ISSUE) + + private val Stubs = + arrayOf( + kotlin( + """ + package androidx.compose.material + + import androidx.compose.runtime.Composable + + @Composable + fun Text(text: String) { + // no-op + } + + @Composable + fun Surface(content: @Composable () -> Unit) { + // no-op + } + + object BottomNavigationDefaults { + val Elevation = 8.dp + } + + enum class BottomDrawerValue { + Closed, + Open, + Expanded + } + """ + .trimIndent() + ), + kotlin( + """ + package androidx.compose.material.ripple + + import androidx.compose.runtime.Composable + + @Composable + fun rememberRipple() + """ + .trimIndent() + ), + ) + + @Test + fun smokeTest() { + lint() + .configureOption(M2ApiDetector.ALLOW_LIST, "Surface") + .files( + *Stubs, + kotlin( + """ + import androidx.compose.material.BottomDrawerValue + import androidx.compose.material.BottomNavigationDefaults + import androidx.compose.material.Text + import androidx.compose.material.ripple.rememberRipple + import androidx.compose.runtime.Composable + + @Composable + fun example() { + Text("Hello, world!") + } + + @Composable + fun allowedExample() { + Surface { + + } + } + + @Composable + fun composite() { + Surface { + val ripple = rememberRipple() + Text("Hello, world!") + val elevation = BottomNavigationDefaults.Elevation + val drawerValue = BottomDrawerValue.Closed + } + } + """ + ) + .indented() + ) + .allowCompilationErrors() + .run() + .expect( + """ + src/test.kt:9: Warning: Compose Material 2 (M2) is succeeded by Material 3 (M3). Please use M3 APIs. + See https://slackhq.github.io/compose-lints/rules/#use-material-3 for more information. [ComposeM2Api] + Text("Hello, world!") + ~~~~~~~~~~~~~~~~~~~~~ + src/test.kt:23: Warning: Compose Material 2 (M2) is succeeded by Material 3 (M3). Please use M3 APIs. + See https://slackhq.github.io/compose-lints/rules/#use-material-3 for more information. [ComposeM2Api] + Text("Hello, world!") + ~~~~~~~~~~~~~~~~~~~~~ + src/test.kt:24: Warning: Compose Material 2 (M2) is succeeded by Material 3 (M3). Please use M3 APIs. + See https://slackhq.github.io/compose-lints/rules/#use-material-3 for more information. [ComposeM2Api] + val elevation = BottomNavigationDefaults.Elevation + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test.kt:25: Warning: Compose Material 2 (M2) is succeeded by Material 3 (M3). Please use M3 APIs. + See https://slackhq.github.io/compose-lints/rules/#use-material-3 for more information. [ComposeM2Api] + val drawerValue = BottomDrawerValue.Closed + ~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 4 warnings + """ + .trimIndent() + ) + } +} diff --git a/docs/rules.md b/docs/rules.md index 74eb2695..a5bc30b1 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -294,3 +294,39 @@ Composed modifiers may be created outside of composition, shared across elements More info: [Modifier extensions](https://developer.android.com/reference/kotlin/androidx/compose/ui/package-summary#extension-functions), [Composed modifiers in Jetpack Compose by Jorge Castillo](https://jorgecastillo.dev/composed-modifiers-in-jetpack-compose) and [Composed modifiers in API guidelines](https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#composed-modifiers) Related rule: [`ComposeComposableModifier`](https://github.com/slackhq/compose-lints/blob/main/compose-lint-checks/src/main/java/slack/lint/compose/ModifierComposableDetector.kt) + +### Use Material 3 + +Rule: [`ComposeM2Api`](https://github.com/slackhq/compose-lints/blob/main/compose-lint-checks/src/main/java/slack/lint/compose/M2ApiDetector.kt) + +Material 3 (M3) reached stable in October 2022. In apps that have migrated to M3, there may be `androidx.compose.material` (M2) APIs still remaining on the classpath from libraries or dependencies that can cause confusing imports due to the many similar or colliding Composable names in the two libraries. The `ComposeM2Api` rule can prevent these from being used. + +!!! warning "Lint Configuration" + This rule is set to `IGNORE` by default and is **opt-in**. You can enable and make it an error via `lint` in Gradle. + ```kotlin + android { + lint { + enable += "ComposeM2Api" + error += "ComposeM2Api" + } + } + ``` + More lint configuration docs can be found [here](https://developer.android.com/studio/write/lint#gradle). + +!!! note "Allow-list Configuration" + To allow certain APIs (i.e. for incremental migration), you can configure a `allowed-m2-apis` option in `lint.xml`. + ```xml + + + ``` + +**Related docs links** + +- Announcement post: https://material.io/blog/material-3-compose-stable +- Docs: https://m3.material.io/develop/android/jetpack-compose +- Migration guide: https://developer.android.com/jetpack/compose/themes/material2-material3 +- Guidance: https://developer.android.com/jetpack/compose/themes/material3 +- Reply (primary sample app): https://github.com/android/compose-samples/tree/main/Reply +- More samples: https://github.com/android/compose-samples +