Skip to content

Commit

Permalink
Implement M2ApiDetector (#75)
Browse files Browse the repository at this point in the history
* 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 <chrisbanes@users.noreply.github.com>

* 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 <chrisbanes@users.noreply.github.com>
  • Loading branch information
ZacSweers and chrisbanes authored Mar 7, 2023
1 parent 6522969 commit 0bdba39
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class ComposeLintsIssueRegistry : IssueRegistry() {
ModifierMissingDetector.ISSUE,
ModifierReusedDetector.ISSUE,
ModifierWithoutDefaultDetector.ISSUE,
M2ApiDetector.ISSUE,
MultipleContentEmittersDetector.ISSUE,
MutableParametersDetector.ISSUE,
ParameterOrderDetector.ISSUE,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<M2ApiDetector>()
)
.setOptions(listOf(ALLOW_LIST))
}

override fun getApplicableUastTypes() =
listOf<Class<out UElement>>(
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),
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Issue> = 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()
)
}
}
36 changes: 36 additions & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<issue id="ComposeM2Api">
<option name="allowed-m2-apis" value="Text,Surface" />
</issue>
```

**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

0 comments on commit 0bdba39

Please sign in to comment.