diff --git a/rules/src/main/java/com/farsitel/bazaar/lintrules/BazaarIssueRegistry.kt b/rules/src/main/java/com/farsitel/bazaar/lintrules/BazaarIssueRegistry.kt index e6a9777..08ba4ec 100644 --- a/rules/src/main/java/com/farsitel/bazaar/lintrules/BazaarIssueRegistry.kt +++ b/rules/src/main/java/com/farsitel/bazaar/lintrules/BazaarIssueRegistry.kt @@ -19,12 +19,16 @@ 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.farsitel.bazaar.lintrules.rules.PluginsOverrideInNonFinalClassDetector +import com.farsitel.bazaar.lintrules.rules.PreferNotFunctionDetector /* * The list of issues that will be checked when running lint. */ class BazaarIssueRegistry : IssueRegistry() { - override val issues = listOf(PluginsOverrideInNonFinalClassDetector.ISSUE) + override val issues = listOf( + PluginsOverrideInNonFinalClassDetector.ISSUE, + PreferNotFunctionDetector.ISSUE, + ) override val api: Int get() = CURRENT_API diff --git a/rules/src/main/java/com/farsitel/bazaar/lintrules/rules/PreferNotFunctionDetector.kt b/rules/src/main/java/com/farsitel/bazaar/lintrules/rules/PreferNotFunctionDetector.kt new file mode 100644 index 0000000..33c554f --- /dev/null +++ b/rules/src/main/java/com/farsitel/bazaar/lintrules/rules/PreferNotFunctionDetector.kt @@ -0,0 +1,62 @@ +package com.farsitel.bazaar.lintrules.rules + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Detector.UastScanner +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.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.isKotlin +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UPrefixExpression +import org.jetbrains.uast.UastPrefixOperator + +/** + * Detector spotting Kotlin logical-not `!` operator; prefer `not()` instead. + */ +class PreferNotFunctionDetector : Detector(), UastScanner { + + override fun getApplicableUastTypes(): List> { + return listOf(UPrefixExpression::class.java) + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + return object : UElementHandler() { + override fun visitPrefixExpression(node: UPrefixExpression) { + if (isKotlin(context.uastFile?.lang).not()) { + return + } + if ((node.operator == UastPrefixOperator.LOGICAL_NOT).not()) { + return + } + context.report( + ISSUE, + node, + context.getLocation(node), + "Do not use `!`; use `not()` instead" + ) + } + } + } + + companion object { + @JvmField + val ISSUE: Issue = Issue.create( + id = "PreferNotFunction", + briefDescription = "Prefer not() over !", + explanation = """ + Do not use the `!` logical-not operator. Use the `not()` function instead. + """, + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.ERROR, + implementation = Implementation( + PreferNotFunctionDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} diff --git a/rules/src/test/kotlin/com/farsitel/bazaar/lintrules/PreferNotFunctionDetectorTest.kt b/rules/src/test/kotlin/com/farsitel/bazaar/lintrules/PreferNotFunctionDetectorTest.kt new file mode 100644 index 0000000..101748a --- /dev/null +++ b/rules/src/test/kotlin/com/farsitel/bazaar/lintrules/PreferNotFunctionDetectorTest.kt @@ -0,0 +1,137 @@ +package com.farsitel.bazaar.lintrules + +import com.android.tools.lint.checks.infrastructure.TestFiles.java +import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin +import com.android.tools.lint.checks.infrastructure.TestLintResult +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint +import com.android.tools.lint.checks.infrastructure.TestMode +import com.farsitel.bazaar.lintrules.rules.PreferNotFunctionDetector +import org.intellij.lang.annotations.Language +import org.junit.Test + +class PreferNotFunctionDetectorTest { + + @Test + fun testBangOperatorError() { + baseTest( + """ + package test.pkg + + fun check(flag: Boolean): Boolean { + return !flag + } + """ + ) + .expect(EXPECTED_BANG_ERROR) + } + + @Test + fun testBangInIfConditionError() { + baseTest( + """ + package test.pkg + + fun check(flag: Boolean) { + if (!flag) { + return + } + } + """ + ) + .expectErrorCount(1) + } + + @Test + fun testMultipleBangOperatorsError() { + baseTest( + """ + package test.pkg + + fun check(a: Boolean, b: Boolean): Boolean { + return !a && !b + } + """ + ) + .expectErrorCount(2) + } + + @Test + fun testNotFunctionClean() { + baseTest( + """ + package test.pkg + + fun check(flag: Boolean): Boolean { + return flag.not() + } + """ + ) + .expectClean() + } + + @Test + fun testNotEqualsClean() { + baseTest( + """ + package test.pkg + + fun check(a: Int, b: Int): Boolean { + return a != b + } + """ + ) + .expectClean() + } + + @Test + fun testNotNullAssertClean() { + baseTest( + """ + package test.pkg + + fun check(value: String?): String { + return value!! + } + """ + ) + .expectClean() + } + + @Test + fun testJavaBangClean() { + lint() + .files( + java( + """ + package test.pkg; + + class Test { + boolean check(boolean flag) { + return !flag; + } + } + """ + ).indented() + ) + .issues(PreferNotFunctionDetector.ISSUE) + .skipTestModes(TestMode.SUPPRESSIBLE) + .run() + .expectClean() + } + + private fun baseTest(@Language("kotlin") source: String): TestLintResult { + return lint() + .files(kotlin("src/test/pkg/Test.kt", source).indented()) + .issues(PreferNotFunctionDetector.ISSUE) + .skipTestModes(TestMode.SUPPRESSIBLE) + .run() + } + + companion object { + const val EXPECTED_BANG_ERROR = """ +src/test/pkg/Test.kt:4: Error: Do not use !; use not() instead [PreferNotFunction] + return !flag + ~~~~~ +1 errors, 0 warnings""" + } +}