diff --git a/app/build.gradle b/app/build.gradle index 19ae875..f660671 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,12 +4,12 @@ plugins { } android { - compileSdk 30 + compileSdk 32 defaultConfig { applicationId "ru.otus.homework.androidlint" minSdk 21 - targetSdk 30 + targetSdk 32 versionCode 1 versionName "1.0" @@ -32,6 +32,7 @@ android { } dependencies { + implementation project(':library') implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' implementation 'androidx.core:core-ktx:1.6.0' diff --git a/build.gradle b/build.gradle index 611ef82..5c5fb84 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,38 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext{ + + detekt_version = '1.20.0' + kotlinVersion = '1.6.21' + // Current release: Chipmunk / AGP 7.2 + gradlePluginVersion = '7.0.2' + lintVersion = '30.2.0' + // lintVersion = '30.2.0-rc02' + // Upcoming lint target: Dolphin / AGP 7.3 + // gradlePluginVersion = '7.3.0-beta01' + // lintVersion = '30.3.0-beta01' + // Upcoming lint target: Electric Eel / AGP 7.4 + // gradlePluginVersion = '7.4.0-alpha02' + // lintVersion = '30.4.0-alpha02' + } repositories { google() mavenCentral() + maven { url 'https://jitpack.io' } } dependencies { - classpath "com.android.tools.build:gradle:7.0.2" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30" + classpath "com.android.tools.build:gradle:$gradlePluginVersion" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$detekt_version" + // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } +allprojects { + apply from: "$rootDir/detekt.gradle" +} task clean(type: Delete) { delete rootProject.buildDir diff --git a/detekt.gradle b/detekt.gradle new file mode 100644 index 0000000..f1548d3 --- /dev/null +++ b/detekt.gradle @@ -0,0 +1,26 @@ +apply plugin: "io.gitlab.arturbosch.detekt" + +detekt { + toolVersion = detekt_version + input = files( + "src/main/java", + "src/main/kotlin" + ) + failFast = false + parallel = true + buildUponDefaultConfig = false + ignoreFailures = false + reports { + xml { + enabled = true + destination = file("build/reports/detekt.xml") + } + html { + enabled = true + destination = file("build/reports/detekt.html") + } + } + dependencies { + detektPlugins "io.gitlab.arturbosch.detekt:detekt-formatting:$detekt_version" + } +} \ No newline at end of file diff --git a/library/.gitignore b/library/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/library/.gitignore @@ -0,0 +1 @@ +/build diff --git a/library/build.gradle b/library/build.gradle new file mode 100644 index 0000000..9816565 --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,22 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 32 + defaultConfig { + minSdkVersion 19 + targetSdkVersion 32 + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + lintOptions { + checkDependencies true + } +} + +/** Package the given lint checks library into this AAR */ +dependencies { + implementation project(':lintRules') + lintPublish project(':lintRules') +} diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..843049f --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/lintRules/.gitignore b/lintRules/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/lintRules/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/lintRules/build.gradle b/lintRules/build.gradle new file mode 100644 index 0000000..3876f1b --- /dev/null +++ b/lintRules/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java-library' + id 'org.jetbrains.kotlin.jvm' + id 'kotlin' + id 'com.android.lint' +} + +lintOptions { + htmlReport true + htmlOutput file("lint-report.html") + textReport true + absolutePaths false + ignoreTestSources true +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + compileOnly "com.android.tools.lint:lint-api:$lintVersion" + compileOnly "com.android.tools.lint:lint-checks:$lintVersion" + + testImplementation "junit:junit:4.13.2" + testImplementation "com.android.tools.lint:lint:$lintVersion" + testImplementation "com.android.tools.lint:lint-tests:$lintVersion" +} \ No newline at end of file diff --git a/lintRules/src/main/java/ru/otus/lintrules/GlobalScopeDetector.kt b/lintRules/src/main/java/ru/otus/lintrules/GlobalScopeDetector.kt new file mode 100644 index 0000000..43ba100 --- /dev/null +++ b/lintRules/src/main/java/ru/otus/lintrules/GlobalScopeDetector.kt @@ -0,0 +1,51 @@ +package ru.otus.lintrules + +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 org.jetbrains.uast.* + +@Suppress("UnstableApiUsage") +class GlobalScopeDetector : Detector(), UastScanner { + override fun getApplicableUastTypes(): List> { + return listOf(USimpleNameReferenceExpression::class.java) + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + return object : UElementHandler() { + + override fun visitSimpleNameReferenceExpression(node: USimpleNameReferenceExpression){ + if (node.identifier.contains("GlobalScope")) { + context.report( + ISSUE, node, context.getLocation(node), + "Don't use GlobalScope!!!" + ) + } + } + } + } + + companion object { + @JvmField + val ISSUE: Issue = Issue.create( + id = "GlobalScopeWarningId", + briefDescription = "GlobalScope usage warning", + explanation = """ + You shouldn't use GlobalScope! Instead of use a custom scope. + """, + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + GlobalScopeDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} \ No newline at end of file diff --git a/lintRules/src/main/java/ru/otus/lintrules/SampleCodeDetector.kt b/lintRules/src/main/java/ru/otus/lintrules/SampleCodeDetector.kt new file mode 100644 index 0000000..63ea14a --- /dev/null +++ b/lintRules/src/main/java/ru/otus/lintrules/SampleCodeDetector.kt @@ -0,0 +1,64 @@ +package ru.otus.lintrules + +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 org.jetbrains.uast.UElement +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.evaluateString + +@Suppress("UnstableApiUsage") +class SampleCodeDetector : Detector(), UastScanner { + override fun getApplicableUastTypes(): List> { + return listOf(ULiteralExpression::class.java) + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + 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. + 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 + ) + ) + } +} \ No newline at end of file diff --git a/lintRules/src/main/java/ru/otus/lintrules/SampleIssueRegistry.kt b/lintRules/src/main/java/ru/otus/lintrules/SampleIssueRegistry.kt new file mode 100644 index 0000000..65da816 --- /dev/null +++ b/lintRules/src/main/java/ru/otus/lintrules/SampleIssueRegistry.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2021 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 ru.otus.lintrules + +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 + +/* + * The list of issues that will be checked when running lint. + */ +@Suppress("UnstableApiUsage") +class SampleIssueRegistry : IssueRegistry() { + override val issues = listOf(SampleCodeDetector.ISSUE) + + 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 +} diff --git a/lintRules/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/lintRules/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry new file mode 100644 index 0000000..34b2e04 --- /dev/null +++ b/lintRules/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry @@ -0,0 +1 @@ +ru.otus.lintrules.SampleIssueRegistry diff --git a/lintRules/src/test/kotlin/GlobalScopeDetectorTest.kt b/lintRules/src/test/kotlin/GlobalScopeDetectorTest.kt new file mode 100644 index 0000000..48a22e0 --- /dev/null +++ b/lintRules/src/test/kotlin/GlobalScopeDetectorTest.kt @@ -0,0 +1,46 @@ +package ru.otus.lint.checks + +import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint +import org.junit.Before +import org.junit.Test +import ru.otus.lintrules.GlobalScopeDetector + +@Suppress("UnstableApiUsage") +class GlobalScopeDetectorTest { + + private lateinit var task: TestLintTask + + @Before + fun prepare() { + task = lint().allowMissingSdk(true) + } + + @Test + fun checkFoundGlobalScope() { + task.files( + kotlin( + """ + package test.pkg + class TestClass1 { + + init { + GlobalScope.launch { } + } + } + """ + ).indented() + ) + .issues(GlobalScopeDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass1.kt:5: Warning: Don't use GlobalScope!!! [GlobalScopeWarningId] + GlobalScope.launch { } + ~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } +} \ No newline at end of file diff --git a/lintRules/src/test/kotlin/SampleCodeDetectorTest.kt b/lintRules/src/test/kotlin/SampleCodeDetectorTest.kt new file mode 100644 index 0000000..0b6dc5a --- /dev/null +++ b/lintRules/src/test/kotlin/SampleCodeDetectorTest.kt @@ -0,0 +1,45 @@ +package ru.otus.lint.checks + +import com.android.tools.lint.checks.infrastructure.TestFiles.java +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint +import org.junit.Before +import org.junit.Test +import ru.otus.lintrules.SampleCodeDetector + +@Suppress("UnstableApiUsage") +class SampleCodeDetectorTest { + + private lateinit var task: TestLintTask + + @Before + fun prepare() { + task = lint().allowMissingSdk(true) + } + + @Test + fun testBasic() { + task.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 + """ + ) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 4eccc00..7fb01bd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,3 +8,5 @@ dependencyResolutionManagement { } rootProject.name = "Android Lint" include ':app' +include ':lintRules' +include ':library'