Merge pull request #899 from SimonMarquis/lint-test-detector

Create `TestMethodDetector` Lint to detect common naming patterns we want to avoid
pull/999/head
Don Turner 8 months ago committed by GitHub
commit f2bc315eaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -47,7 +47,7 @@ jobs:
path: '**/build/outputs/apk/**/*.apk'
- name: Run local tests
run: ./gradlew testDemoDebug testProdDebug
run: ./gradlew testDemoDebug testProdDebug :lint:test
test:
runs-on: ubuntu-latest

@ -122,6 +122,8 @@ kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-cor
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
lint-api = { group = "com.android.tools.lint", name = "lint-api", version.ref = "lint" }
lint-checks = { group = "com.android.tools.lint", name = "lint-checks", version.ref = "lint" }
lint-tests = { group = "com.android.tools.lint", name = "lint-tests", version.ref = "lint" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" }
protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }

@ -38,4 +38,7 @@ tasks.withType<KotlinCompile>().configureEach {
dependencies {
compileOnly(libs.kotlin.stdlib)
compileOnly(libs.lint.api)
testImplementation(libs.lint.checks)
testImplementation(libs.lint.tests)
testImplementation(kotlin("test"))
}

@ -14,18 +14,20 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.lint.designsystem
package com.google.samples.apps.nowinandroid.lint
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.client.api.Vendor
import com.android.tools.lint.detector.api.CURRENT_API
import com.google.samples.apps.nowinandroid.lint.designsystem.DesignSystemDetector
/**
* An issue registry that checks for incorrect usages of Compose Material APIs over equivalents in
* the Now in Android design system module.
*/
class DesignSystemIssueRegistry : IssueRegistry() {
override val issues = listOf(DesignSystemDetector.ISSUE)
class NiaIssueRegistry : IssueRegistry() {
override val issues = listOf(
DesignSystemDetector.ISSUE,
TestMethodNameDetector.FORMAT,
TestMethodNameDetector.PREFIX,
)
override val api: Int = CURRENT_API

@ -0,0 +1,126 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.lint
import com.android.tools.lint.detector.api.AnnotationInfo
import com.android.tools.lint.detector.api.AnnotationUsageInfo
import com.android.tools.lint.detector.api.Category.Companion.TESTING
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.LintFix
import com.android.tools.lint.detector.api.Scope.JAVA_FILE
import com.android.tools.lint.detector.api.Scope.TEST_SOURCES
import com.android.tools.lint.detector.api.Severity.WARNING
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.android.tools.lint.detector.api.TextFormat.RAW
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UElement
import java.util.EnumSet
import kotlin.io.path.Path
/**
* A detector that checks for common patterns in naming the test methods:
* - [detectPrefix] removes unnecessary "test" prefix in all unit test.
* - [detectFormat] Checks the `given_when_then` format of Android instrumented tests (backticks are not supported).
*/
class TestMethodNameDetector : Detector(), SourceCodeScanner {
override fun applicableAnnotations() = listOf("org.junit.Test")
override fun visitAnnotationUsage(
context: JavaContext,
element: UElement,
annotationInfo: AnnotationInfo,
usageInfo: AnnotationUsageInfo,
) {
val method = usageInfo.referenced as? PsiMethod ?: return
method.detectPrefix(context, usageInfo)
method.detectFormat(context, usageInfo)
}
private fun JavaContext.isAndroidTest() = Path("androidTest") in file.toPath()
private fun PsiMethod.detectPrefix(
context: JavaContext,
usageInfo: AnnotationUsageInfo,
) {
if (!name.startsWith("test")) return
context.report(
issue = PREFIX,
scope = usageInfo.usage,
location = context.getNameLocation(this),
message = PREFIX.getBriefDescription(RAW),
quickfixData = LintFix.create()
.name("Remove prefix")
.replace().pattern("""test[\s_]*""")
.with("")
.autoFix()
.build(),
)
}
private fun PsiMethod.detectFormat(
context: JavaContext,
usageInfo: AnnotationUsageInfo,
) {
if (!context.isAndroidTest()) return
if ("""[^\W_]+(_[^\W_]+){1,2}""".toRegex().matches(name)) return
context.report(
issue = FORMAT,
scope = usageInfo.usage,
location = context.getNameLocation(this),
message = FORMAT.getBriefDescription(RAW),
)
}
companion object {
private fun issue(
id: String,
briefDescription: String,
explanation: String,
): Issue = Issue.create(
id = id,
briefDescription = briefDescription,
explanation = explanation,
category = TESTING,
priority = 5,
severity = WARNING,
implementation = Implementation(
TestMethodNameDetector::class.java,
EnumSet.of(JAVA_FILE, TEST_SOURCES),
),
)
@JvmField
val PREFIX: Issue = issue(
id = "TestMethodPrefix",
briefDescription = "Test method starts with `test`",
explanation = "Test method should not start with `test`.",
)
@JvmField
val FORMAT: Issue = issue(
id = "TestMethodFormat",
briefDescription = "Test method does not follow the `given_when_then` or `when_then` format",
explanation = "Test method should follow the `given_when_then` or `when_then` format.",
)
}
}

@ -14,4 +14,4 @@
# limitations under the License.
#
com.google.samples.apps.nowinandroid.lint.designsystem.DesignSystemIssueRegistry
com.google.samples.apps.nowinandroid.lint.NiaIssueRegistry

@ -0,0 +1,123 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.lint
import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
import com.google.samples.apps.nowinandroid.lint.TestMethodNameDetector.Companion.FORMAT
import com.google.samples.apps.nowinandroid.lint.TestMethodNameDetector.Companion.PREFIX
import org.junit.Test
class TestMethodNameDetectorTest {
@Test
fun `detect prefix`() {
lint().issues(PREFIX)
.files(
JUNIT_TEST_STUB,
kotlin(
"""
import org.junit.Test
class Test {
@Test
fun foo() = Unit
@Test
fun test_foo() = Unit
@Test
fun `test foo`() = Unit
}
""",
).indented(),
)
.run()
.expect(
"""
src/Test.kt:6: Warning: Test method starts with test [TestMethodPrefix]
fun test_foo() = Unit
~~~~~~~~
src/Test.kt:8: Warning: Test method starts with test [TestMethodPrefix]
fun `test foo`() = Unit
~~~~~~~~~~
0 errors, 2 warnings
""".trimIndent(),
)
.expectFixDiffs(
"""
Autofix for src/Test.kt line 6: Remove prefix:
@@ -6 +6
- fun test_foo() = Unit
+ fun foo() = Unit
Autofix for src/Test.kt line 8: Remove prefix:
@@ -8 +8
- fun `test foo`() = Unit
+ fun `foo`() = Unit
""".trimIndent(),
)
}
@Test
fun `detect format`() {
lint().issues(FORMAT)
.files(
JUNIT_TEST_STUB,
kotlin(
"src/androidTest/com/example/Test.kt",
"""
import org.junit.Test
class Test {
@Test
fun when_then() = Unit
@Test
fun given_when_then() = Unit
@Test
fun foo() = Unit
@Test
fun foo_bar_baz_qux() = Unit
@Test
fun `foo bar baz`() = Unit
}
""",
).indented(),
)
.run()
.expect(
"""
src/androidTest/com/example/Test.kt:9: Warning: Test method does not follow the given_when_then or when_then format [TestMethodFormat]
fun foo() = Unit
~~~
src/androidTest/com/example/Test.kt:11: Warning: Test method does not follow the given_when_then or when_then format [TestMethodFormat]
fun foo_bar_baz_qux() = Unit
~~~~~~~~~~~~~~~
src/androidTest/com/example/Test.kt:13: Warning: Test method does not follow the given_when_then or when_then format [TestMethodFormat]
fun `foo bar baz`() = Unit
~~~~~~~~~~~~~
0 errors, 3 warnings
""".trimIndent(),
)
}
private companion object {
private val JUNIT_TEST_STUB: TestFile = kotlin(
"""
package org.junit
annotation class Test
""",
).indented()
}
}
Loading…
Cancel
Save