parent
0af797f613
commit
d8af223958
@ -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…
Reference in new issue