From cb67cc2c636b80783ded2ef391dc3ebb3bc99e9b Mon Sep 17 00:00:00 2001 From: Tor Norbye Date: Sat, 1 Jun 2024 19:28:55 -0700 Subject: [PATCH] Add a few more examples, including a Kotlin Analysis API usage Also update dependencies. And format with ktfmt. --- .../example/lint/checks/AvoidDateDetector.kt | 89 +++++++++++++++ .../lint/checks/NotNullAssertionDetector.kt | 84 ++++++++++++++ .../example/lint/checks/SampleCodeDetector.kt | 103 +++++++++--------- .../lint/checks/SampleIssueRegistry.kt | 25 +++-- .../lint/checks/AvoidDateDetectorTest.kt | 88 +++++++++++++++ .../checks/NotNullAssertionDetectorTest.kt | 60 ++++++++++ .../lint/checks/SampleCodeDetectorTest.kt | 53 ++++----- gradle.properties | 1 - gradle/libs.versions.toml | 6 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 10 files changed, 415 insertions(+), 96 deletions(-) create mode 100644 checks/src/main/java/com/example/lint/checks/AvoidDateDetector.kt create mode 100644 checks/src/main/java/com/example/lint/checks/NotNullAssertionDetector.kt create mode 100644 checks/src/test/java/com/example/lint/checks/AvoidDateDetectorTest.kt create mode 100644 checks/src/test/java/com/example/lint/checks/NotNullAssertionDetectorTest.kt diff --git a/checks/src/main/java/com/example/lint/checks/AvoidDateDetector.kt b/checks/src/main/java/com/example/lint/checks/AvoidDateDetector.kt new file mode 100644 index 00000000..cdda8bd4 --- /dev/null +++ b/checks/src/main/java/com/example/lint/checks/AvoidDateDetector.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 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 + * + * http://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.example.lint.checks + +import com.android.tools.lint.detector.api.Category +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.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression + +class AvoidDateDetector : Detector(), SourceCodeScanner { + companion object Issues { + private val IMPLEMENTATION = + Implementation(AvoidDateDetector::class.java, Scope.JAVA_FILE_SCOPE) + + @JvmField + val ISSUE = + Issue.create( + id = "OldDate", + briefDescription = "Avoid Date and Calendar", + explanation = + """ + The `java.util.Date` and `java.util.Calendar` classes should not be used; instead \ + use the `java.time` package, such as `LocalDate` and `LocalTime`. + """, + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.ERROR, + androidSpecific = true, + implementation = IMPLEMENTATION, + ) + } + + // java.util.Date() + override fun getApplicableConstructorTypes(): List = listOf("java.util.Date") + + override fun visitConstructor( + context: JavaContext, + node: UCallExpression, + constructor: PsiMethod, + ) { + context.report( + ISSUE, + node, + context.getLocation(node), + "Don't use `Date`; use `java.time.*` instead", + fix() + .alternatives( + fix().replace().all().with("java.time.LocalTime.now()").shortenNames().build(), + fix().replace().all().with("java.time.LocalDate.now()").shortenNames().build(), + fix().replace().all().with("java.time.LocalDateTime.now()").shortenNames().build(), + ), + ) + } + + // java.util.Calendar.getInstance() + override fun getApplicableMethodNames(): List = listOf("getInstance") + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + val evaluator = context.evaluator + if (!evaluator.isMemberInClass(method, "java.util.Calendar")) { + return + } + context.report( + ISSUE, + node, + context.getLocation(node), + "Don't use `Calendar.getInstance`; use `java.time.*` instead", + ) + } +} diff --git a/checks/src/main/java/com/example/lint/checks/NotNullAssertionDetector.kt b/checks/src/main/java/com/example/lint/checks/NotNullAssertionDetector.kt new file mode 100644 index 00000000..8d0fe2de --- /dev/null +++ b/checks/src/main/java/com/example/lint/checks/NotNullAssertionDetector.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 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 + * + * http://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.example.lint.checks + +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.Implementation +import com.android.tools.lint.detector.api.Incident +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.SourceCodeScanner +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.codegen.optimization.common.analyze +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UPostfixExpression + +class NotNullAssertionDetector : Detector(), SourceCodeScanner { + companion object Issues { + private val IMPLEMENTATION = + Implementation(NotNullAssertionDetector::class.java, Scope.JAVA_FILE_SCOPE) + + @JvmField + val ISSUE = + Issue.create( + id = "NotNullAssertion", + briefDescription = "Avoid `!!`", + explanation = + """ + Do not use the `!!` operator. It can lead to null pointer exceptions. \ + Please use the `?` operator instead, or assign to a local variable with \ + `?:` initialization if necessary. + """, + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.WARNING, + implementation = IMPLEMENTATION, + ) + } + + override fun getApplicableUastTypes(): List>? { + return listOf(UPostfixExpression::class.java) + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + return object : UElementHandler() { + override fun visitPostfixExpression(node: UPostfixExpression) { + if (node.operator.text == "!!") { + var message = "Do not use `!!`" + + // Kotlin Analysis API example + val sourcePsi = node.operand.sourcePsi + if (sourcePsi is KtExpression) { + analyze(sourcePsi) { + val type = sourcePsi.getKtType() + if (type != null && !type.canBeNull) { + message += " -- it's not even needed here" + } + } + } + + val incident = Incident(ISSUE, node, context.getLocation(node), message) + context.report(incident) + } + } + } + } +} diff --git a/checks/src/main/java/com/example/lint/checks/SampleCodeDetector.kt b/checks/src/main/java/com/example/lint/checks/SampleCodeDetector.kt index 1a27e22c..1f1efdca 100644 --- a/checks/src/main/java/com/example/lint/checks/SampleCodeDetector.kt +++ b/checks/src/main/java/com/example/lint/checks/SampleCodeDetector.kt @@ -29,64 +29,61 @@ import org.jetbrains.uast.ULiteralExpression import org.jetbrains.uast.evaluateString /** - * Sample detector showing how to analyze Kotlin/Java code. This example - * flags all string literals in the code that contain the word "lint". + * Sample detector showing how to analyze Kotlin/Java code. This example flags all string literals + * in the code that contain the word "lint". */ -@Suppress("UnstableApiUsage") class SampleCodeDetector : Detector(), UastScanner { - override fun getApplicableUastTypes(): List> { - return listOf(ULiteralExpression::class.java) - } + override fun getApplicableUastTypes(): List> { + return listOf(ULiteralExpression::class.java) + } - override fun createUastHandler(context: JavaContext): UElementHandler { - // Note: Visiting UAST nodes is a pretty general purpose mechanism; - // Lint has specialized support to do common things like "visit every class - // that extends a given super class or implements a given interface", and - // "visit every call site that calls a method by a given name" etc. - // Take a careful look at UastScanner and the various existing lint check - // implementations before doing things the "hard way". - // Also be aware of context.getJavaEvaluator() which provides a lot of - // utility functionality. - return object : UElementHandler() { - override fun visitLiteralExpression(node: ULiteralExpression) { - val string = node.evaluateString() ?: return - if (string.contains("lint") && string.matches(Regex(".*\\blint\\b.*"))) { - context.report( - ISSUE, node, context.getLocation(node), - "This code mentions `lint`: **Congratulations**" - ) - } - } + override fun createUastHandler(context: JavaContext): UElementHandler { + // Note: Visiting UAST nodes is a pretty general purpose mechanism; + // Lint has specialized support to do common things like "visit every class + // that extends a given super class or implements a given interface", and + // "visit every call site that calls a method by a given name" etc. + // Take a careful look at UastScanner and the various existing lint check + // implementations before doing things the "hard way". + // Also be aware of context.getJavaEvaluator() which provides a lot of + // utility functionality. + return object : UElementHandler() { + override fun visitLiteralExpression(node: ULiteralExpression) { + val string = node.evaluateString() ?: return + if (string.contains("lint") && string.matches(Regex(".*\\blint\\b.*"))) { + context.report( + ISSUE, + node, + context.getLocation(node), + "This code mentions `lint`: **Congratulations**", + ) } + } } + } - companion object { - /** - * Issue describing the problem and pointing to the detector - * implementation. - */ - @JvmField - val ISSUE: Issue = Issue.create( - // ID: used in @SuppressLint warnings etc - id = "SampleId", - // Title -- shown in the IDE's preference dialog, as category headers in the - // Analysis results window, etc - briefDescription = "Lint Mentions", - // Full explanation of the issue; you can use some markdown markup such as - // `monospace`, *italic*, and **bold**. - explanation = """ - This check highlights string literals in code which mentions the word `lint`. \ - Blah blah blah. + companion object { + /** Issue describing the problem and pointing to the detector implementation. */ + @JvmField + val ISSUE: Issue = + Issue.create( + // ID: used in @SuppressLint warnings etc + id = "SampleId", + // Title -- shown in the IDE's preference dialog, as category headers in the + // Analysis results window, etc + briefDescription = "Lint Mentions", + // Full explanation of the issue; you can use some markdown markup such as + // `monospace`, *italic*, and **bold**. + explanation = + """ + This check highlights string literals in code which mentions the word `lint`. \ + Blah blah blah. - Another paragraph here. - """, // no need to .trimIndent(), lint does that automatically - category = Category.CORRECTNESS, - priority = 6, - severity = Severity.WARNING, - implementation = Implementation( - SampleCodeDetector::class.java, - Scope.JAVA_FILE_SCOPE - ) - ) - } + Another paragraph here. + """, // no need to .trimIndent(), lint does that automatically + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation(SampleCodeDetector::class.java, Scope.JAVA_FILE_SCOPE), + ) + } } diff --git a/checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.kt b/checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.kt index aafaafa9..042eac3c 100644 --- a/checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.kt +++ b/checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.kt @@ -22,21 +22,22 @@ import com.android.tools.lint.detector.api.CURRENT_API /* * The list of issues that will be checked when running lint. */ -@Suppress("UnstableApiUsage") class SampleIssueRegistry : IssueRegistry() { - override val issues = listOf(SampleCodeDetector.ISSUE) + override val issues = + listOf(SampleCodeDetector.ISSUE, AvoidDateDetector.ISSUE, NotNullAssertionDetector.ISSUE) - override val api: Int - get() = CURRENT_API + override val api: Int + get() = CURRENT_API - override val minApi: Int - get() = 8 // works with Studio 4.1 or later; see com.android.tools.lint.detector.api.Api / ApiKt + override val minApi: Int + get() = 8 // works with Studio 4.1 or later; see com.android.tools.lint.detector.api.Api / ApiKt - // Requires lint API 30.0+; if you're still building for something - // older, just remove this property. - override val vendor: Vendor = Vendor( - vendorName = "Android Open Source Project", - feedbackUrl = "https://github.com/googlesamples/android-custom-lint-rules/issues", - contact = "https://github.com/googlesamples/android-custom-lint-rules" + // Requires lint API 30.0+; if you're still building for something + // older, just remove this property. + override val vendor: Vendor = + Vendor( + vendorName = "Android Open Source Project", + feedbackUrl = "https://github.com/googlesamples/android-custom-lint-rules/issues", + contact = "https://github.com/googlesamples/android-custom-lint-rules", ) } diff --git a/checks/src/test/java/com/example/lint/checks/AvoidDateDetectorTest.kt b/checks/src/test/java/com/example/lint/checks/AvoidDateDetectorTest.kt new file mode 100644 index 00000000..8f8fe5fb --- /dev/null +++ b/checks/src/test/java/com/example/lint/checks/AvoidDateDetectorTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2024 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 + * + * http://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.example.lint.checks + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +class AvoidDateDetectorTest : LintDetectorTest() { + override fun getDetector(): Detector = AvoidDateDetector() + + override fun getIssues(): List = listOf(AvoidDateDetector.ISSUE) + + fun testDocumentationExample() { + lint() + .files( + kotlin( + """ + package test.pkg + import java.util.Date + fun test() { + val date = Date() + } + """ + ) + .indented(), + kotlin( + """ + package test.pkg + import java.util.Calendar + + fun test2() { + val calendar = Calendar.getInstance() + } + """ + ) + .indented(), + ) + .allowMissingSdk() + .run() + .expect( + """ + src/test/pkg/test.kt:4: Error: Don't use Date; use java.time.* instead [OldDate] + val date = Date() + ~~~~~~ + src/test/pkg/test2.kt:5: Error: Don't use Calendar.getInstance; use java.time.* instead [OldDate] + val calendar = Calendar.getInstance() + ~~~~~~~~~~~~~~~~~~~~~~ + 2 errors, 0 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/test/pkg/test.kt line 4: Replace with java.time.LocalTime.now(): + @@ -2 +2 + + import java.time.LocalTime + @@ -4 +5 + - val date = Date() + + val date = LocalTime.now() + Fix for src/test/pkg/test.kt line 4: Replace with java.time.LocalDate.now(): + @@ -2 +2 + + import java.time.LocalDate + @@ -4 +5 + - val date = Date() + + val date = LocalDate.now() + Fix for src/test/pkg/test.kt line 4: Replace with java.time.LocalDateTime.now(): + @@ -2 +2 + + import java.time.LocalDateTime + @@ -4 +5 + - val date = Date() + + val date = LocalDateTime.now() + """ + ) + } +} diff --git a/checks/src/test/java/com/example/lint/checks/NotNullAssertionDetectorTest.kt b/checks/src/test/java/com/example/lint/checks/NotNullAssertionDetectorTest.kt new file mode 100644 index 00000000..3b951866 --- /dev/null +++ b/checks/src/test/java/com/example/lint/checks/NotNullAssertionDetectorTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 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 + * + * http://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.example.lint.checks + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +class NotNullAssertionDetectorTest : LintDetectorTest() { + override fun getDetector(): Detector { + return NotNullAssertionDetector() + } + + override fun getIssues(): List { + return listOf(NotNullAssertionDetector.ISSUE) + } + + fun testDocumentationExample() { + lint() + .files( + kotlin( + """ + package test.pkg + + fun test(s: String?, t: String) { + s?.plus(s) + s!!.plus(s) + t!!.plus(t) + } + """ + ) + .indented() + ) + .run() + .expect( + """ + src/test/pkg/test.kt:5: Warning: Do not use !! [NotNullAssertion] + s!!.plus(s) + ~~~ + src/test/pkg/test.kt:6: Warning: Do not use !! -- it's not even needed here [NotNullAssertion] + t!!.plus(t) + ~~~ + 0 errors, 2 warnings + """ + ) + } +} diff --git a/checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.kt b/checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.kt index fe476397..3f9afa37 100644 --- a/checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.kt +++ b/checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.kt @@ -19,31 +19,32 @@ import com.android.tools.lint.checks.infrastructure.TestFiles.java import com.android.tools.lint.checks.infrastructure.TestLintTask.lint import org.junit.Test -@Suppress("UnstableApiUsage") class SampleCodeDetectorTest { - @Test - fun testBasic() { - lint().files( - java( - """ - package test.pkg; - public class TestClass1 { - // In a comment, mentioning "lint" has no effect - private static String s1 = "Ignore non-word usages: linting"; - private static String s2 = "Let's say it: lint"; - } - """ - ).indented() - ) - .issues(SampleCodeDetector.ISSUE) - .run() - .expect( - """ - src/test/pkg/TestClass1.java:5: Warning: This code mentions lint: Congratulations [SampleId] - private static String s2 = "Let's say it: lint"; - ~~~~~~~~~~~~~~~~~~~~ - 0 errors, 1 warnings - """ - ) - } + @Test + fun testBasic() { + lint() + .files( + java( + """ + package test.pkg; + public class TestClass1 { + // In a comment, mentioning "lint" has no effect + private static String s1 = "Ignore non-word usages: linting"; + private static String s2 = "Let's say it: lint"; + } + """ + ) + .indented() + ) + .issues(SampleCodeDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:5: Warning: This code mentions lint: Congratulations [SampleId] + private static String s2 = "Let's say it: lint"; + ~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } } diff --git a/gradle.properties b/gradle.properties index b07f583a..55e33e63 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,6 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -android.defaults.buildfeatures.buildconfig=true android.nonFinalResIds=false android.nonTransitiveRClass=true org.gradle.configuration-cache=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3b94864e..ac7a1126 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] -agp = '8.1.2' -lint = '31.1.2' # = agp + 23.0.0 -kotlin = '1.9.10' +agp = "8.4.1" +lint = '31.4.1' # = agp + 23.0.0 +kotlin = '1.9.23' junit = '4.13.2' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 59bc51a2..48c0a02c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists