Add `TestMethodDetector` with tests

pull/899/head
Simon Marquis 1 year ago
parent 0af797f613
commit d8af223958

@ -25,6 +25,9 @@ class NiaIssueRegistry : IssueRegistry() {
override val issues = listOf(
DesignSystemDetector.ISSUE,
TestMethodDetector.UNDERSCORE,
TestMethodDetector.FORMAT,
TestMethodDetector.PREFIX,
)
override val api: Int = CURRENT_API

@ -0,0 +1,160 @@
/*
* 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.
* - [detectUnderscore] removes underscores in JVM unit test (and add backticks if necessary).
* - [detectFormat] Checks the `given_when_then` format of Android instrumented tests (backticks are not supported).
*/
class TestMethodDetector : 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)
method.detectUnderscore(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_]+_[^\W_]+$""".toRegex().matches(name)) return
context.report(
issue = FORMAT,
scope = usageInfo.usage,
location = context.getNameLocation(this),
message = FORMAT.getBriefDescription(RAW),
)
}
private fun PsiMethod.detectUnderscore(
context: JavaContext,
usageInfo: AnnotationUsageInfo,
) {
if (context.isAndroidTest()) return
if ("_" !in name) return
context.report(
issue = UNDERSCORE,
scope = usageInfo.usage,
location = context.getNameLocation(this),
message = UNDERSCORE.getBriefDescription(RAW),
quickfixData = LintFix.create()
.name("Replace underscores with spaces")
.replace()
.range(context.getNameLocation(this))
.with(
name.replace("_", " ")
.removeSurrounding("`")
.let { """`$it`""" },
)
.autoFix()
.build(),
)
}
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(
TestMethodDetector::class.java,
EnumSet.of(JAVA_FILE, TEST_SOURCES),
),
)
@JvmField
val UNDERSCORE: Issue = issue(
id = "TestMethodUnderscore",
briefDescription = "Test method contains underscores",
explanation = "Test methods should not contains underscores.",
)
@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` format",
explanation = "Test method should follow the `given_when_then` format.",
)
}
}

@ -0,0 +1,155 @@
/*
* 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.TestMethodDetector.Companion.FORMAT
import com.google.samples.apps.nowinandroid.lint.TestMethodDetector.Companion.PREFIX
import org.junit.Test
class TestMethodDetectorTest {
@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 [TestMethodWithTestPrefix]
fun test_foo() = Unit
~~~~~~~~
src/Test.kt:8: Warning: Test method starts with test [TestMethodWithTestPrefix]
fun `test foo`() = Unit
~~~~~~~~~~
0 errors, 2 warnings
""".trimIndent(),
)
.expectFixDiffs(
"""
Fix for src/Test.kt line 6: Remove underscores:
@@ -6 +6
- fun test_foo() = Unit
+ fun foo() = Unit
Fix for src/Test.kt line 8: Remove underscores:
@@ -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 given_when_then() = Unit
@Test
fun given_foo_when_bar_then_baz() = Unit
}
""",
).indented(),
)
.run()
.expect(
"""
src/androidTest/com/example/Test.kt:6: Warning: Test method does not follow the given_when_then format [TestMethodGivenWhenThenFormatTest]
fun given_foo_when_bar_then_baz() = Unit
~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 1 warnings
""".trimIndent(),
)
}
@Test
fun `detect underscores`() {
lint().issues(UNDERSCORES)
.files(
JUNIT_TEST_STUB,
kotlin(
"""
import org.junit.Test
class Test {
@Test
fun foo() = Unit
@Test
fun foo_bar_baz() = Unit
@Test
fun `foo_bar_baz`() = Unit
}
""",
).indented(),
)
.run()
.expect(
"""
src/Test.kt:6: Warning: Test method contains underscores [TestMethodContainsUnderscore]
fun foo_bar_baz() = Unit
~~~~~~~~~~~
src/Test.kt:8: Warning: Test method contains underscores [TestMethodContainsUnderscore]
fun `foo_bar_baz`() = Unit
~~~~~~~~~~~~~
0 errors, 2 warnings
""".trimIndent(),
)
.expectFixDiffs(
"""
Autofix for src/Test.kt line 6: Replace underscores with spaces:
@@ -6 +6
- fun foo_bar_baz() = Unit
+ fun `foo bar baz`() = Unit
Autofix for src/Test.kt line 8: Replace underscores with spaces:
@@ -8 +8
- fun `foo_bar_baz`() = Unit
+ fun `foo bar baz`() = Unit
""".trimIndent(),
)
}
private companion object {
private val JUNIT_TEST_STUB: TestFile = kotlin(
"""
package org.junit
annotation class Test
""",
).indented()
}
}
Loading…
Cancel
Save