├── .github
├── funding.yml
└── workflows
│ ├── build.yml
│ ├── publish-release.yml
│ └── publish-snapshot.yml
├── renovate.json
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── lint-rules-android
├── gradle.properties
└── build.gradle.kts
├── lint-rules-kotlin
├── gradle.properties
└── build.gradle.kts
├── lint-rules-rxjava2
├── gradle.properties
└── build.gradle.kts
├── lint-rules-kotlin-lint
├── gradle.properties
├── src
│ ├── main
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── vanniktech
│ │ │ └── lintrules
│ │ │ └── kotlin
│ │ │ ├── IssueRegistry.kt
│ │ │ └── KotlinRequireNotNullUseMessageDetector.kt
│ └── test
│ │ └── kotlin
│ │ └── com
│ │ └── vanniktech
│ │ └── lintrules
│ │ └── rxjava2
│ │ ├── KotlinRequireNotNullUseMessageDetectorTest.kt
│ │ └── IssueRegistryTest.kt
└── build.gradle.kts
├── lint-rules-android-lint
├── gradle.properties
├── src
│ ├── test
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── vanniktech
│ │ │ └── lintrules
│ │ │ └── android
│ │ │ ├── ExtensionsTest.kt
│ │ │ ├── Stubs.kt
│ │ │ ├── InvalidAccessibilityDetectorTest.kt
│ │ │ ├── JcenterDetectorTest.kt
│ │ │ ├── MissingXmlHeaderDetectorTest.kt
│ │ │ ├── IssueRegistryTest.kt
│ │ │ ├── ColorCasingDetectorTest.kt
│ │ │ ├── SuperfluousNameSpaceDetectorTest.kt
│ │ │ ├── UnusedMergeAttributesDetectorTest.kt
│ │ │ ├── FormalGermanDetectorTest.kt
│ │ │ ├── DefaultLayoutAttributeDetectorTest.kt
│ │ │ ├── AndroidDetectorTest.kt
│ │ │ ├── WrongViewIdFormatDetectorTest.kt
│ │ │ ├── AlertDialogUsageDetectorTest.kt
│ │ │ ├── WrongDrawableNameDetectorTest.kt
│ │ │ ├── WrongMenuIdFormatDetectorTest.kt
│ │ │ ├── InvalidImportDetectorTest.kt
│ │ │ ├── ConstraintLayoutToolsEditorAttributeDetectorTest.kt
│ │ │ ├── QuotesDetectorTest.kt
│ │ │ ├── XmlSpacingDetectorTest.kt
│ │ │ ├── WrongTestMethodNameDetectorTest.kt
│ │ │ ├── ImplicitStringPlaceholderDetectorTest.kt
│ │ │ └── UnsupportedLayoutAttributeDetectorTest.kt
│ └── main
│ │ └── kotlin
│ │ └── com
│ │ └── vanniktech
│ │ └── lintrules
│ │ └── android
│ │ ├── MatchingIdFixer.kt
│ │ ├── SuperfluousDeclarationDetector.kt
│ │ ├── SuperfluousPaddingDeclarationDetector.kt
│ │ ├── InvalidStringDetector.kt
│ │ ├── ImplicitStringPlaceholderDetector.kt
│ │ ├── SuperfluousMarginDeclarationDetector.kt
│ │ ├── FormalGermanDetector.kt
│ │ ├── StringXmlDetector.kt
│ │ ├── ElementCollectReporter.kt
│ │ ├── WrongLayoutNameDetector.kt
│ │ ├── DefaultLayoutAttributeDetector.kt
│ │ ├── JcenterDetector.kt
│ │ ├── MissingXmlHeaderDetector.kt
│ │ ├── WrongViewIdFormatDetector.kt
│ │ ├── ConstraintLayoutToolsEditorAttributeDetector.kt
│ │ ├── InvalidAccessibilityDetector.kt
│ │ ├── WrongDrawableNameDetector.kt
│ │ ├── ColorCasingDetector.kt
│ │ ├── UnusedMergeAttributesDetector.kt
│ │ ├── SuperfluousNameSpaceDetector.kt
│ │ ├── MatchingViewIdDetector.kt
│ │ ├── XmlSpacingDetector.kt
│ │ ├── UnsupportedLayoutAttributeDetector.kt
│ │ ├── WrongMenuIdFormatDetector.kt
│ │ ├── MatchingMenuIdDetector.kt
│ │ ├── AlertDialogUsageDetector.kt
│ │ ├── WrongConstraintLayoutUsageDetector.kt
│ │ ├── IssueRegistry.kt
│ │ ├── TodoDetector.kt
│ │ ├── AssertjDetector.kt
│ │ ├── MissingScrollbarsDetector.kt
│ │ ├── WrongTestMethodNameDetector.kt
│ │ ├── QuotesDetector.kt
│ │ ├── StringNotCapitalizedDetector.kt
│ │ ├── WrongGlobalIconColorDetector.kt
│ │ ├── RawColorDetector.kt
│ │ ├── InvalidImportDetector.kt
│ │ ├── AndroidDetector.kt
│ │ ├── LayoutFileNameMatchesClassDetector.kt
│ │ ├── extensions.kt
│ │ ├── ErroneousLayoutAttributeDetector.kt
│ │ └── RawDimenDetector.kt
└── build.gradle.kts
├── lint-rules-rxjava2-lint
├── gradle.properties
├── src
│ ├── test
│ │ ├── resources
│ │ │ ├── dagger-2.14.1.jar
│ │ │ ├── rxjava-2.1.7.jar
│ │ │ ├── rxandroid-2.0.1.jar
│ │ │ └── reactive-streams-1.0.2.jar
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── vanniktech
│ │ │ └── lintrules
│ │ │ └── rxjava2
│ │ │ ├── AnyExtensions.kt
│ │ │ ├── RxJava2DisposableAddAllCallDetectorTest.kt
│ │ │ ├── RxJava2DisposableDisposeCallDetectorTest.kt
│ │ │ ├── IssueRegistryTest.kt
│ │ │ └── RxJava2DefaultSchedulerDetectorTest.kt
│ └── main
│ │ └── kotlin
│ │ └── com
│ │ └── vanniktech
│ │ └── lintrules
│ │ └── rxjava2
│ │ ├── IssueRegistry.kt
│ │ ├── RxJava2DisposableAddAllCallDetector.kt
│ │ ├── RxJava2DisposableDisposeCallDetector.kt
│ │ ├── RxJava2SubscribeMissingOnErrorDetector.kt
│ │ ├── RxJava2DefaultSchedulerDetector.kt
│ │ ├── RxJava2SchedulersFactoryCallDetector.kt
│ │ ├── RxJava2MissingCompositeDisposableClearDetector.kt
│ │ └── RxJava2MethodMissingCheckReturnValueDetector.kt
└── build.gradle.kts
├── lint-rules-kotlin.md
├── settings.gradle.kts
├── lint.xml
├── .gitignore
├── .editorconfig
├── README.md
├── gradle.properties
├── lint-rules-rxjava2.md
└── gradlew.bat
/.github/funding.yml:
--------------------------------------------------------------------------------
1 | github: [vanniktech]
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base",
4 | ":disableDependencyDashboard"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vanniktech/lint-rules/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/lint-rules-android/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=Lint Rules Android
2 | POM_ARTIFACT_ID=lint-rules-android
3 | POM_PACKAGING=aar
--------------------------------------------------------------------------------
/lint-rules-kotlin/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=Lint Rules Kotlin
2 | POM_ARTIFACT_ID=lint-rules-kotlin
3 | POM_PACKAGING=aar
4 |
--------------------------------------------------------------------------------
/lint-rules-rxjava2/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=Lint Rules RxJava 2
2 | POM_ARTIFACT_ID=lint-rules-rxjava2
3 | POM_PACKAGING=aar
--------------------------------------------------------------------------------
/lint-rules-kotlin-lint/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=Lint Rules Kotlin
2 | POM_ARTIFACT_ID=lint-rules-kotlin-lint
3 | POM_PACKAGING=jar
4 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=Lint Rules Android
2 | POM_ARTIFACT_ID=lint-rules-android-lint
3 | POM_PACKAGING=jar
4 |
--------------------------------------------------------------------------------
/lint-rules-rxjava2-lint/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=Lint Rules RxJava 2
2 | POM_ARTIFACT_ID=lint-rules-rxjava2-lint
3 | POM_PACKAGING=jar
4 |
--------------------------------------------------------------------------------
/lint-rules-kotlin.md:
--------------------------------------------------------------------------------
1 | - **KotlinRequireNotNullUseMessage** - The default generated message from requireNotNull often lacks context, hence it's best to provide a custom message.
--------------------------------------------------------------------------------
/lint-rules-rxjava2-lint/src/test/resources/dagger-2.14.1.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vanniktech/lint-rules/HEAD/lint-rules-rxjava2-lint/src/test/resources/dagger-2.14.1.jar
--------------------------------------------------------------------------------
/lint-rules-rxjava2-lint/src/test/resources/rxjava-2.1.7.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vanniktech/lint-rules/HEAD/lint-rules-rxjava2-lint/src/test/resources/rxjava-2.1.7.jar
--------------------------------------------------------------------------------
/lint-rules-rxjava2-lint/src/test/resources/rxandroid-2.0.1.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vanniktech/lint-rules/HEAD/lint-rules-rxjava2-lint/src/test/resources/rxandroid-2.0.1.jar
--------------------------------------------------------------------------------
/lint-rules-rxjava2-lint/src/test/resources/reactive-streams-1.0.2.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vanniktech/lint-rules/HEAD/lint-rules-rxjava2-lint/src/test/resources/reactive-streams-1.0.2.jar
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | include(":lint-rules-android")
2 | include(":lint-rules-android-lint")
3 | include(":lint-rules-kotlin")
4 | include(":lint-rules-kotlin-lint")
5 | include(":lint-rules-rxjava2")
6 | include(":lint-rules-rxjava2-lint")
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-all.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # generated files
2 | bin/
3 | gen/
4 |
5 | # Java class files
6 | *.class
7 |
8 | # Sublime
9 | *.sublime-workspace
10 |
11 | # Android Studio
12 | .idea
13 | .gradle
14 | build/
15 | *.iml
16 | captures/
17 | local.properties
18 |
19 | # Editor temp files
20 | *~
21 | *.swp
22 |
23 | # OSX file spam
24 | .DS_Store
25 |
26 | # secret signing stuff
27 | keystore-secrets
28 |
29 | paperwork.json
30 |
31 | #lint result
32 | lint-result.xml
33 |
34 | # Oh my zsh plugins
35 | .gradletasknamecache
--------------------------------------------------------------------------------
/lint-rules-kotlin/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | id("com.vanniktech.maven.publish")
4 | }
5 |
6 | android {
7 | namespace = "com.vanniktech.lintruleskotlin"
8 | compileSdk = libs.versions.compileSdk.get().toInt()
9 |
10 | defaultConfig {
11 | minSdk = libs.versions.minSdk.get().toInt()
12 | }
13 |
14 | compileOptions {
15 | sourceCompatibility = JavaVersion.VERSION_11
16 | targetCompatibility = JavaVersion.VERSION_11
17 | }
18 | }
19 |
20 | dependencies {
21 | lintPublish(project(":lint-rules-kotlin-lint"))
22 | }
23 |
--------------------------------------------------------------------------------
/lint-rules-android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | id("com.vanniktech.maven.publish")
4 | }
5 |
6 | android {
7 | namespace = "com.vanniktech.lintrulesandroid"
8 | compileSdk = libs.versions.compileSdk.get().toInt()
9 |
10 | defaultConfig {
11 | minSdk = libs.versions.minSdk.get().toInt()
12 | }
13 |
14 | compileOptions {
15 | sourceCompatibility = JavaVersion.VERSION_11
16 | targetCompatibility = JavaVersion.VERSION_11
17 | }
18 | }
19 |
20 | dependencies {
21 | lintPublish(project(":lint-rules-android-lint"))
22 | }
23 |
--------------------------------------------------------------------------------
/lint-rules-rxjava2/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | id("com.vanniktech.maven.publish")
4 | }
5 |
6 | android {
7 | namespace = "com.vanniktech.lintrulesrxjava2"
8 | compileSdk = libs.versions.compileSdk.get().toInt()
9 |
10 | defaultConfig {
11 | minSdk = libs.versions.minSdk.get().toInt()
12 | }
13 |
14 | compileOptions {
15 | sourceCompatibility = JavaVersion.VERSION_11
16 | targetCompatibility = JavaVersion.VERSION_11
17 | }
18 | }
19 |
20 | dependencies {
21 | lintPublish(project(":lint-rules-rxjava2-lint"))
22 | }
23 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/test/kotlin/com/vanniktech/lintrules/android/ExtensionsTest.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import org.junit.Assert.assertEquals
6 | import org.junit.Test
7 |
8 | class ExtensionsTest {
9 | @Test fun toSnakeCase() {
10 | assertEquals("something", "Something".toSnakeCase())
11 | assertEquals("something_foo", "SomethingFoo".toSnakeCase())
12 | assertEquals("foo2bar_something", "Foo2BarSomething".toSnakeCase())
13 | assertEquals("a_b_c_d_e", "ABCDE".toSnakeCase())
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{kt,kts}]
2 | ktlint_code_style=intellij_idea
3 | indent_size=2
4 | continuation_indent_size=2
5 | ij_kotlin_allow_trailing_comma=true
6 | ij_kotlin_allow_trailing_comma_on_call_site=true
7 | insert_final_newline=true
8 | ktlint_standard_annotation=disabled
9 | ktlint_standard_max-line-length=disabled
10 | ktlint_standard_filename=disabled
11 | ktlint_standard_spacing-between-declarations-with-annotations=disabled
12 | ktlint_standard_blank-line-between-when-conditions=disabled
13 | ktlint_standard_backing-property-naming=disabled
14 | ktlint_standard_kdoc=disabled
15 | ktlint_standard_condition-wrapping=disabled
16 | ktlint_experimental=enabled
--------------------------------------------------------------------------------
/lint-rules-rxjava2-lint/src/test/kotlin/com/vanniktech/lintrules/rxjava2/AnyExtensions.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.rxjava2
4 |
5 | import com.android.tools.lint.checks.infrastructure.TestFiles.bytes
6 |
7 | fun Any.dagger2() = bytes("libs/dagger-2.14.1.jar", javaClass.getResourceAsStream("/dagger-2.14.1.jar").readBytes())
8 | fun Any.reactiveStreams() = bytes("libs/reactive-streams-1.0.2.jar", javaClass.getResourceAsStream("/reactive-streams-1.0.2.jar").readBytes())
9 | fun Any.rxJava2() = bytes("libs/rxjava-2.1.7.jar", javaClass.getResourceAsStream("/rxjava-2.1.7.jar").readBytes())
10 | fun Any.rxAndroid2() = bytes("libs/rxandroid-2.0.1.jar", javaClass.getResourceAsStream("/rxandroid-2.0.1.jar").readBytes())
11 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/MatchingIdFixer.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.detector.api.XmlContext
6 | import com.android.utils.usLocaleCapitalize
7 |
8 | class MatchingIdFixer(context: XmlContext, private val id: String) {
9 | private val layoutName = context.file.name.replace(".xml", "")
10 | val expectedPrefix = layoutName.toLowerCamelCase()
11 |
12 | fun needsFix() = !id.startsWith(expectedPrefix)
13 |
14 | fun fixedId(): String = if (id.startsWith(expectedPrefix, ignoreCase = true)) {
15 | expectedPrefix + id.substring(expectedPrefix.length)
16 | } else {
17 | expectedPrefix + id.usLocaleCapitalize()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lint-rules-kotlin-lint/src/main/kotlin/com/vanniktech/lintrules/kotlin/IssueRegistry.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.kotlin
4 |
5 | import com.android.tools.lint.client.api.Vendor
6 | import com.android.tools.lint.detector.api.CURRENT_API
7 |
8 | internal const val PRIORITY = 10 // Does not matter anyway within Lint.
9 |
10 | class IssueRegistry : com.android.tools.lint.client.api.IssueRegistry() {
11 | override val api get() = CURRENT_API
12 |
13 | override val vendor get() = Vendor(
14 | vendorName = "vanniktech/lint-rules/",
15 | feedbackUrl = "https://github.com/vanniktech/lint-rules/issues",
16 | )
17 |
18 | override val issues get() = listOf(
19 | ISSUE_KOTLIN_REQUIRE_NOT_NULL_USE_MESSAGE,
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on: [push, pull_request, merge_group]
4 |
5 | jobs:
6 | build:
7 | name: JDK ${{ matrix.java_version }}
8 | runs-on: ubuntu-latest
9 |
10 | strategy:
11 | matrix:
12 | java_version: [17]
13 |
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v6
17 |
18 | - name: Gradle Wrapper Validation
19 | uses: gradle/actions/wrapper-validation@v5
20 |
21 | - name: Setup gradle
22 | uses: gradle/gradle-build-action@v3
23 |
24 | - name: Install JDK ${{ matrix.java_version }}
25 | uses: actions/setup-java@v5
26 | with:
27 | distribution: 'zulu'
28 | java-version: ${{ matrix.java_version }}
29 |
30 | - name: Build with Gradle
31 | run: ./gradlew build --stacktrace
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Lint-Rules
2 | ==========
3 |
4 | A set of very opinionated lint rules.
5 |
6 | ## Android Lint Rules
7 |
8 | ```groovy
9 | compile 'com.vanniktech:lint-rules-android:0.25.0'
10 | compile 'com.vanniktech:lint-rules-android:0.26.0-SNAPSHOT'
11 | ```
12 |
13 | [Full list of all checks that are added.](lint-rules-android.md)
14 |
15 | ## Kotlin Lint Rules
16 |
17 | ```groovy
18 | compile 'com.vanniktech:lint-rules-kotlin:0.25.0'
19 | compile 'com.vanniktech:lint-rules-kotlin:0.26.0-SNAPSHOT'
20 | ```
21 |
22 | [Full list of all checks that are added.](lint-rules-kotlin.md)
23 |
24 | ## RxJava 2 Lint Rules
25 |
26 | ```groovy
27 | compile 'com.vanniktech:lint-rules-rxjava2:0.25.0'
28 | compile 'com.vanniktech:lint-rules-rxjava2:0.26.0-SNAPSHOT'
29 | ```
30 |
31 | [Full list of all checks that are added.](lint-rules-rxjava2.md)
32 |
33 | # License
34 |
35 | Copyright (C) 2017 Vanniktech - Niklas Baudy
36 |
37 | Licensed under the Apache License, Version 2.0
38 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | minSdk = "21"
3 | compileSdk = "36"
4 | targetSdk = "36"
5 |
6 | androidgradleplugin = "8.13.2"
7 | lint = "31.13.2"
8 | kotlin = "2.2.21"
9 | ktlint = "1.4.1"
10 |
11 | [libraries]
12 | junit = { module = "junit:junit", version = "4.13.2" }
13 | lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "lint" }
14 | lint-core = { module = "com.android.tools.lint:lint", version.ref = "lint" }
15 | lint-tests = { module = "com.android.tools.lint:lint-tests", version.ref = "lint" }
16 | plugin-androidgradleplugin = { module = "com.android.tools.build:gradle", version.ref = "androidgradleplugin" }
17 | plugin-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version = "0.35.0" }
18 | plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
19 | plugin-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "2.1.0" }
20 |
21 | [plugins]
22 | codequalitytools = { id = "com.vanniktech.code.quality.tools", version = "0.24.0" }
23 |
--------------------------------------------------------------------------------
/lint-rules-rxjava2-lint/src/main/kotlin/com/vanniktech/lintrules/rxjava2/IssueRegistry.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.rxjava2
4 |
5 | import com.android.tools.lint.client.api.Vendor
6 | import com.android.tools.lint.detector.api.CURRENT_API
7 |
8 | internal const val PRIORITY = 10 // Does not matter anyway within Lint.
9 |
10 | class IssueRegistry : com.android.tools.lint.client.api.IssueRegistry() {
11 | override val api get() = CURRENT_API
12 |
13 | override val vendor get() = Vendor(
14 | vendorName = "vanniktech/lint-rules/",
15 | feedbackUrl = "https://github.com/vanniktech/lint-rules/issues",
16 | )
17 |
18 | override val issues get() = listOf(
19 | ISSUE_METHOD_MISSING_CHECK_RETURN_VALUE,
20 | ISSUE_MISSING_COMPOSITE_DISPOSABLE_CLEAR,
21 | ISSUE_DISPOSABLE_ADD_ALL_CALL,
22 | ISSUE_DEFAULT_SCHEDULER,
23 | ISSUE_DISPOSABLE_DISPOSE_CALL,
24 | ISSUE_SUBSCRIBE_MISSING_ON_ERROR,
25 | ISSUE_RAW_SCHEDULER_CALL,
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_8
2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3 |
4 | plugins {
5 | id("org.jetbrains.kotlin.jvm")
6 | id("com.vanniktech.maven.publish")
7 | }
8 |
9 | kotlin {
10 | jvmToolchain {
11 | languageVersion.set(JavaLanguageVersion.of(17))
12 | }
13 | }
14 |
15 | dependencies {
16 | compileOnly(libs.lint.api)
17 | }
18 |
19 | dependencies {
20 | testImplementation(libs.junit)
21 | testImplementation(libs.lint.core)
22 | testImplementation(libs.lint.tests)
23 | }
24 |
25 | tasks.withType().configureEach {
26 | compilerOptions {
27 | // Lint forces Kotlin (regardless of what version the project uses), so this
28 | // forces a matching language level for now. Similar to `targetCompatibility` for Java.
29 | apiVersion.set(KOTLIN_1_8)
30 | languageVersion.set(KOTLIN_1_8)
31 | }
32 | }
33 |
34 | tasks.withType(Jar::class.java).configureEach {
35 | manifest.attributes["Lint-Registry-v2"] = "com.vanniktech.lintrules.android.IssueRegistry"
36 | }
37 |
--------------------------------------------------------------------------------
/lint-rules-rxjava2-lint/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_8
2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3 |
4 | plugins {
5 | id("org.jetbrains.kotlin.jvm")
6 | id("com.vanniktech.maven.publish")
7 | }
8 |
9 | kotlin {
10 | jvmToolchain {
11 | languageVersion.set(JavaLanguageVersion.of(17))
12 | }
13 | }
14 |
15 | dependencies {
16 | compileOnly(libs.lint.api)
17 | }
18 |
19 | dependencies {
20 | testImplementation(libs.junit)
21 | testImplementation(libs.lint.core)
22 | testImplementation(libs.lint.tests)
23 | }
24 |
25 | tasks.withType().configureEach {
26 | compilerOptions {
27 | // Lint forces Kotlin (regardless of what version the project uses), so this
28 | // forces a matching language level for now. Similar to `targetCompatibility` for Java.
29 | apiVersion.set(KOTLIN_1_8)
30 | languageVersion.set(KOTLIN_1_8)
31 | }
32 | }
33 |
34 | tasks.withType(Jar::class.java).configureEach {
35 | manifest.attributes["Lint-Registry-v2"] = "com.vanniktech.lintrules.rxjava2.IssueRegistry"
36 | }
37 |
--------------------------------------------------------------------------------
/.github/workflows/publish-release.yml:
--------------------------------------------------------------------------------
1 | name: Publish Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | publish:
10 |
11 | runs-on: ubuntu-latest
12 | if: github.repository == 'vanniktech/lint-rules'
13 |
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
17 |
18 | - name: Install JDK 17
19 | uses: actions/setup-java@v5
20 | with:
21 | distribution: 'zulu'
22 | java-version: 17
23 |
24 | - name: Setup gradle
25 | uses: gradle/gradle-build-action@v3
26 |
27 | - name: Publish release
28 | run: ./gradlew publishAllPublicationsToMavenCentralRepository
29 | env:
30 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
31 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
32 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }}
33 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
34 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | VERSION_NAME=0.26.0-SNAPSHOT
2 | GROUP=com.vanniktech
3 |
4 | POM_DESCRIPTION=Very opinionated Lint Checks
5 | POM_URL=https://github.com/vanniktech/lint-rules
6 | POM_SCM_URL=https://github.com/vanniktech/lint-rules
7 | POM_SCM_CONNECTION=scm:git@github.com:vanniktech/lint-rules.git
8 | POM_SCM_DEV_CONNECTION=scm:git@github.com:vanniktech/lint-rules.git
9 | POM_LICENCE_NAME=The Apache Software License, Version 2.0
10 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
11 | POM_LICENCE_DIST=repo
12 | POM_DEVELOPER_ID=vanniktech
13 | POM_DEVELOPER_NAME=Niklas Baudy
14 |
15 | android.useAndroidX=true
16 | android.enableJetifier=false
17 |
18 | SONATYPE_HOST=CENTRAL_PORTAL
19 | SONATYPE_AUTOMATIC_RELEASE=true
20 | RELEASE_SIGNING_ENABLED=true
21 |
22 | org.gradle.jvmargs=-Xmx2048m
23 |
24 | android.defaults.buildfeatures.buildconfig=false
25 | android.defaults.buildfeatures.aidl=false
26 | android.defaults.buildfeatures.renderscript=false
27 | android.defaults.buildfeatures.resvalues=false
28 | android.defaults.buildfeatures.shaders=false
29 |
30 | kotlin.stdlib.default.dependency=false
31 |
--------------------------------------------------------------------------------
/lint-rules-kotlin-lint/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_8
2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3 |
4 | plugins {
5 | id("org.jetbrains.kotlin.jvm")
6 | id("com.vanniktech.maven.publish")
7 | }
8 |
9 | kotlin {
10 | jvmToolchain {
11 | languageVersion.set(JavaLanguageVersion.of(17))
12 | }
13 | }
14 |
15 | dependencies {
16 | compileOnly(libs.lint.api)
17 | }
18 |
19 | dependencies {
20 | compileOnly(libs.lint.api)
21 | }
22 |
23 | dependencies {
24 | testImplementation(libs.junit)
25 | testImplementation(libs.lint.core)
26 | testImplementation(libs.lint.tests)
27 | }
28 |
29 | tasks.withType().configureEach {
30 | compilerOptions {
31 | // Lint forces Kotlin (regardless of what version the project uses), so this
32 | // forces a matching language level for now. Similar to `targetCompatibility` for Java.
33 | apiVersion.set(KOTLIN_1_8)
34 | languageVersion.set(KOTLIN_1_8)
35 | }
36 | }
37 |
38 | tasks.withType(Jar::class.java).configureEach {
39 | manifest.attributes["Lint-Registry-v2"] = "com.vanniktech.lintrules.kotlin.IssueRegistry"
40 | }
41 |
--------------------------------------------------------------------------------
/.github/workflows/publish-snapshot.yml:
--------------------------------------------------------------------------------
1 | name: Publish Snapshot
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | publish:
10 |
11 | runs-on: ubuntu-latest
12 | if: github.repository == 'vanniktech/lint-rules'
13 |
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
17 |
18 | - name: Install JDK 17
19 | uses: actions/setup-java@v5
20 | with:
21 | distribution: 'zulu'
22 | java-version: 17
23 |
24 | - name: Setup gradle
25 | uses: gradle/gradle-build-action@v3
26 |
27 | - name: Retrieve version
28 | run: |
29 | echo "VERSION_NAME=$(cat gradle.properties | grep -w "VERSION_NAME" | cut -d'=' -f2)" >> $GITHUB_ENV
30 |
31 | - name: Publish snapshot
32 | run: ./gradlew publishAllPublicationsToMavenCentralRepository
33 | if: endsWith(env.VERSION_NAME, '-SNAPSHOT')
34 | env:
35 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
36 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
37 |
--------------------------------------------------------------------------------
/lint-rules-rxjava2-lint/src/test/kotlin/com/vanniktech/lintrules/rxjava2/RxJava2DisposableAddAllCallDetectorTest.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.rxjava2
4 |
5 | import com.android.tools.lint.checks.infrastructure.TestFiles.java
6 | import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
7 | import org.junit.Test
8 |
9 | class RxJava2DisposableAddAllCallDetectorTest {
10 | @Test fun callingCompositeDisposableAddAll() {
11 | lint()
12 | .files(
13 | rxJava2(),
14 | java(
15 | """
16 | package foo;
17 | import io.reactivex.disposables.CompositeDisposable;
18 | class Example {
19 | public void foo() {
20 | CompositeDisposable cd = null;
21 | cd.addAll();
22 | }
23 | }
24 | """,
25 | ).indented(),
26 | )
27 | .issues(ISSUE_DISPOSABLE_ADD_ALL_CALL)
28 | .run()
29 | .expect(
30 | """
31 | |src/foo/Example.java:6: Warning: Calling addAll instead of add separately [RxJava2DisposableAddAllCall]
32 | | cd.addAll();
33 | | ~~~~~~
34 | |0 errors, 1 warnings
35 | """.trimMargin(),
36 | )
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/SuperfluousDeclarationDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.detector.api.Issue
6 | import com.android.tools.lint.detector.api.LayoutDetector
7 | import com.android.tools.lint.detector.api.XmlContext
8 | import org.w3c.dom.Element
9 | import java.util.HashSet
10 |
11 | abstract class SuperfluousDeclarationDetector(
12 | private val issue: Issue,
13 | private val message: String,
14 | private val applicableSuperfluousAttributes: Collection,
15 | ) : LayoutDetector() {
16 | override fun getApplicableElements() = ALL
17 |
18 | override fun visitElement(context: XmlContext, element: Element) {
19 | val attributes = (0 until element.attributes.length)
20 | .map { element.attributes.item(it) }
21 | .filterNot { it.hasToolsNamespace() }
22 | .filter { applicableSuperfluousAttributes.contains(it.localName) }
23 | .map { it.nodeValue }
24 | .toList()
25 |
26 | if (attributes.size == applicableSuperfluousAttributes.size && HashSet(attributes).size == 1) {
27 | // Replacing with a Lint fix isn't possible yet. https://issuetracker.google.com/issues/74599279
28 | context.report(issue, element, context.getLocation(element), message)
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/test/kotlin/com/vanniktech/lintrules/android/Stubs.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.checks.infrastructure.TestFiles
6 |
7 | val stubJUnitTest = TestFiles.java(
8 | """
9 | package org.junit;
10 |
11 | public @interface Test { }
12 | """,
13 | ).indented()
14 |
15 | val stubJUnitIgnore = TestFiles.java(
16 | """
17 | package org.junit;
18 |
19 | public @interface Test { }
20 | """,
21 | ).indented()
22 |
23 | val stubAnnotationTest = TestFiles.java(
24 | """
25 | package my.custom;
26 |
27 | public @interface Test { }
28 | """,
29 | ).indented()
30 |
31 | val stubAnnotationSomething = TestFiles.java(
32 | """
33 | package my.custom;
34 |
35 | public @interface Something { }
36 | """,
37 | ).indented()
38 |
39 | fun viewBindingProject() = TestFiles.gradle(
40 | """
41 | apply plugin: 'com.android.library'
42 |
43 | android {
44 | buildFeatures {
45 | viewBinding = true
46 | }
47 | }
48 | """,
49 | )
50 | .indented()
51 |
52 | fun resourcePrefix(prefix: String) = TestFiles.gradle(
53 | """
54 | apply plugin: 'com.android.library'
55 |
56 | android {
57 | resourcePrefix '$prefix'
58 | }
59 | """,
60 | )
61 | .indented()
62 |
--------------------------------------------------------------------------------
/lint-rules-kotlin-lint/src/test/kotlin/com/vanniktech/lintrules/rxjava2/KotlinRequireNotNullUseMessageDetectorTest.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.kotlin
4 |
5 | import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
6 | import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
7 | import org.junit.Test
8 |
9 | class KotlinRequireNotNullUseMessageDetectorTest {
10 | @Test fun requireNotNull() {
11 | lint()
12 | .files(
13 | kotlin(
14 | """
15 | fun test(value: Int?) {
16 | requireNotNull(value)
17 | }
18 | """,
19 | ).indented(),
20 | )
21 | .issues(ISSUE_KOTLIN_REQUIRE_NOT_NULL_USE_MESSAGE)
22 | .run()
23 | .expect(
24 | """
25 | |src/test.kt:2: Warning: Provide a message [KotlinRequireNotNullUseMessage]
26 | | requireNotNull(value)
27 | | ~~~~~~~~~~~~~~
28 | |0 errors, 1 warnings
29 | """.trimMargin(),
30 | )
31 | }
32 |
33 | @Test fun requireNotNullWithMessage() {
34 | lint()
35 | .files(
36 | kotlin(
37 | """
38 | fun test(value: Int?) {
39 | requireNotNull(value) { "Foo" }
40 | }
41 | """,
42 | ).indented(),
43 | )
44 | .issues(ISSUE_KOTLIN_REQUIRE_NOT_NULL_USE_MESSAGE)
45 | .run()
46 | .expectClean()
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lint-rules-rxjava2-lint/src/test/kotlin/com/vanniktech/lintrules/rxjava2/RxJava2DisposableDisposeCallDetectorTest.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.rxjava2
4 |
5 | import com.android.tools.lint.checks.infrastructure.TestFiles.java
6 | import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
7 | import org.junit.Test
8 |
9 | class RxJava2DisposableDisposeCallDetectorTest {
10 | @Test fun callingCompositeDisposableDispose() {
11 | lint()
12 | .files(
13 | rxJava2(),
14 | java(
15 | """
16 | package foo;
17 |
18 | import io.reactivex.disposables.CompositeDisposable;
19 |
20 | class Example {
21 | public void foo() {
22 | CompositeDisposable cd = null;
23 | cd.dispose();
24 | }
25 | }
26 | """,
27 | ).indented(),
28 | )
29 | .issues(ISSUE_DISPOSABLE_DISPOSE_CALL)
30 | .run()
31 | .expect(
32 | """
33 | |src/foo/Example.java:8: Warning: Calling dispose instead of clear [RxJava2DisposableDisposeCall]
34 | | cd.dispose();
35 | | ~~~~~~~
36 | |0 errors, 1 warnings
37 | """.trimMargin(),
38 | )
39 | .expectFixDiffs(
40 | """
41 | |Fix for src/foo/Example.java line 7: Fix it:
42 | |@@ -8 +8
43 | |- cd.dispose();
44 | |+ cd.clear();
45 | """.trimMargin(),
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/SuperfluousPaddingDeclarationDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.SdkConstants.ATTR_PADDING_BOTTOM
6 | import com.android.SdkConstants.ATTR_PADDING_END
7 | import com.android.SdkConstants.ATTR_PADDING_START
8 | import com.android.SdkConstants.ATTR_PADDING_TOP
9 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
10 | import com.android.tools.lint.detector.api.Implementation
11 | import com.android.tools.lint.detector.api.Issue
12 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
13 | import com.android.tools.lint.detector.api.Severity.WARNING
14 | import java.util.Arrays.asList
15 |
16 | val ISSUE_SUPERFLUOUS_PADDING_DECLARATION = Issue.create(
17 | "SuperfluousPaddingDeclaration",
18 | "Flags padding declarations that can be simplified.",
19 | "Instead of using start-, end-, bottom- and top paddings, padding can be used.",
20 | CORRECTNESS,
21 | PRIORITY,
22 | WARNING,
23 | Implementation(SuperfluousPaddingDeclarationDetector::class.java, RESOURCE_FILE_SCOPE),
24 | )
25 |
26 | class SuperfluousPaddingDeclarationDetector :
27 | SuperfluousDeclarationDetector(
28 | applicableSuperfluousAttributes = asList(
29 | ATTR_PADDING_TOP,
30 | ATTR_PADDING_BOTTOM,
31 | ATTR_PADDING_START,
32 | ATTR_PADDING_END,
33 | ),
34 | issue = ISSUE_SUPERFLUOUS_PADDING_DECLARATION,
35 | message = "Should be using padding instead.",
36 | )
37 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/test/kotlin/com/vanniktech/lintrules/android/InvalidAccessibilityDetectorTest.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.checks.infrastructure.TestFiles.xml
6 | import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
7 | import org.junit.Test
8 |
9 | class InvalidAccessibilityDetectorTest {
10 | @Test fun contentDescriptionNull() {
11 | lint()
12 | .files(
13 | xml(
14 | "res/layout/ids.xml",
15 | """
16 |
21 | """,
22 | ).indented(),
23 | )
24 | .issues(ISSUE_INVALID_ACCESSIBILITY)
25 | .run()
26 | .expect(
27 | """
28 | res/layout/ids.xml:5: Warning: Either set a proper accessibility text or use importantForAccessibility [InvalidAccessibility]
29 | android:contentDescription="@null"/>
30 | ~~~~~
31 | 0 errors, 1 warnings
32 | """.trimMargin(),
33 | )
34 | .expectFixDiffs(
35 | """
36 | Autofix for res/layout/ids.xml line 5: Change:
37 | @@ -5 +5
38 | - android:contentDescription="@null" />
39 | + android:importantForAccessibility="no" />
40 | """.trimMargin(),
41 | )
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lint-rules-rxjava2.md:
--------------------------------------------------------------------------------
1 | - **RxJava2DefaultScheduler** - Calling this method will rely on a default scheduler. This is not necessary the best default. Being explicit and taking the overload for passing one is preferred.
2 | - **RxJava2DisposableAddAllCall** - Instead of using addAll(), add() should be used separately for each Disposable.
3 | - **RxJava2DisposableDisposeCall** - Instead of using dispose(), clear() should be used. Calling clear will result in a CompositeDisposable that can be used further to add more Disposables. When using dispose() this is not the case.
4 | - **RxJava2MethodMissingCheckReturnValue** - Methods returning RxJava Reactive Types should be annotated with the @CheckReturnValue annotation. Static analyze tools such as Lint or ErrorProne can detect when the return value of a method is not used. This is usually an indication of a bug. If this is done on purpose (e.g. fire & forget) it should be stated explicitly.
5 | - **RxJava2MissingCompositeDisposableClear** - A class is using CompositeDisposable and not calling clear(). This can leave operations running and even cause memory leaks. It's best to always call clear() once you're done. e.g. in onDestroy() for Activitys.
6 | - **RxJava2SchedulersFactoryCall** - Injecting the Schedulers instead of accessing them via the factory methods has the benefit that unit testing is way easier. Instead of overriding them via the Plugin mechanism we can just pass a custom Scheduler.
7 | - **RxJava2SubscribeMissingOnError** - When calling the subscribe() method an error Consumer should always be used. Otherwise errors might be thrown and may crash the application or get forwarded to the Plugin Error handler.
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/InvalidStringDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
6 | import com.android.tools.lint.detector.api.Implementation
7 | import com.android.tools.lint.detector.api.Issue
8 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
9 | import com.android.tools.lint.detector.api.Severity.WARNING
10 | import com.android.tools.lint.detector.api.XmlContext
11 | import org.w3c.dom.Node
12 |
13 | val ISSUE_INVALID_STRING = Issue.create(
14 | "InvalidString",
15 | "Marks invalid translation strings.",
16 | "A translation string is invalid if it contains new lines instead of the escaped \\n or if it contains trailing whitespace.",
17 | CORRECTNESS,
18 | PRIORITY,
19 | WARNING,
20 | Implementation(InvalidStringDetector::class.java, RESOURCE_FILE_SCOPE),
21 | )
22 |
23 | class InvalidStringDetector : StringXmlDetector() {
24 | override fun checkText(context: XmlContext, node: Node, textNode: Node) {
25 | val text = textNode.nodeValue
26 | val message = when {
27 | text.contains("\n") -> "Text contains new line."
28 | text.length != text.trim().length -> "Text contains trailing whitespace."
29 | else -> null
30 | }
31 |
32 | message?.let {
33 | val fix = fix().replace().name("Fix it").text(text).with(text.trim()).autoFix().build()
34 | context.report(ISSUE_INVALID_STRING, node, context.getLocation(node), it, fix)
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/ImplicitStringPlaceholderDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
6 | import com.android.tools.lint.detector.api.Implementation
7 | import com.android.tools.lint.detector.api.Issue
8 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
9 | import com.android.tools.lint.detector.api.Severity.WARNING
10 | import com.android.tools.lint.detector.api.XmlContext
11 | import org.w3c.dom.Node
12 |
13 | val ISSUE_IMPLICIT_STRING_PLACEHOLDER = Issue.create(
14 | "ImplicitStringPlaceholder",
15 | "Marks implicit placeholders in strings without an index.",
16 | "It's better and more explicit to use numbered placeholders.",
17 | CORRECTNESS,
18 | PRIORITY,
19 | WARNING,
20 | Implementation(ImplicitStringPlaceholderDetector::class.java, RESOURCE_FILE_SCOPE),
21 | )
22 |
23 | class ImplicitStringPlaceholderDetector : StringXmlDetector() {
24 | override fun checkText(context: XmlContext, node: Node, textNode: Node) {
25 | var index = 0
26 | Regex("%s|%d").findAll(textNode.nodeValue).forEach { match ->
27 | val old = match.value
28 | val new = "%${++index}$" + match.value.last()
29 | val fix = fix().replace().name("Fix $old with $new").text(old).with(new).autoFix().build()
30 | context.report(ISSUE_IMPLICIT_STRING_PLACEHOLDER, node, context.getLocation(textNode, match.range.first, match.range.last + 1), "Implicit placeholder", fix)
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/SuperfluousMarginDeclarationDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM
6 | import com.android.SdkConstants.ATTR_LAYOUT_MARGIN_END
7 | import com.android.SdkConstants.ATTR_LAYOUT_MARGIN_START
8 | import com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP
9 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
10 | import com.android.tools.lint.detector.api.Implementation
11 | import com.android.tools.lint.detector.api.Issue
12 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
13 | import com.android.tools.lint.detector.api.Severity.WARNING
14 | import java.util.Arrays.asList
15 |
16 | val ISSUE_SUPERFLUOUS_MARGIN_DECLARATION = Issue.create(
17 | "SuperfluousMarginDeclaration",
18 | "Flags margin declarations that can be simplified.",
19 | "Instead of using start-, end-, bottom- and top margins, layout_margin can be used.",
20 | CORRECTNESS,
21 | PRIORITY,
22 | WARNING,
23 | Implementation(SuperfluousMarginDeclarationDetector::class.java, RESOURCE_FILE_SCOPE),
24 | )
25 |
26 | class SuperfluousMarginDeclarationDetector :
27 | SuperfluousDeclarationDetector(
28 | applicableSuperfluousAttributes = asList(
29 | ATTR_LAYOUT_MARGIN_TOP,
30 | ATTR_LAYOUT_MARGIN_BOTTOM,
31 | ATTR_LAYOUT_MARGIN_START,
32 | ATTR_LAYOUT_MARGIN_END,
33 | ),
34 | issue = ISSUE_SUPERFLUOUS_MARGIN_DECLARATION,
35 | message = "Should be using layout_margin instead.",
36 | )
37 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/test/kotlin/com/vanniktech/lintrules/android/JcenterDetectorTest.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.checks.infrastructure.TestFiles.gradle
6 | import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
7 | import org.junit.Test
8 |
9 | class JcenterDetectorTest {
10 | @Test fun jcenter() {
11 | lint()
12 | .files(
13 | gradle(
14 | """
15 | buildscript {
16 | repositories {
17 | jcenter()
18 | }
19 | }
20 | """,
21 | ).indented(),
22 | )
23 | .issues(ISSUE_JCENTER)
24 | .run()
25 | .expect(
26 | """
27 | |build.gradle:3: Warning: Don't use jcenter() [JCenter]
28 | | jcenter()
29 | | ~~~~~~~~~
30 | |0 errors, 1 warnings
31 | """.trimMargin(),
32 | )
33 | .expectFixDiffs(
34 | """
35 | |Fix for build.gradle line 3: Replace with mavenCentral():
36 | |@@ -3 +3
37 | |- jcenter()
38 | |+ mavenCentral()
39 | """.trimMargin(),
40 | )
41 | }
42 |
43 | @Test fun noJcenter() {
44 | lint()
45 | .files(
46 | gradle(
47 | """
48 | buildscript {
49 | repositories {
50 | mavenCentral()
51 | }
52 | }
53 | """,
54 | ).indented(),
55 | )
56 | .issues(ISSUE_JCENTER)
57 | .run()
58 | .expectClean()
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/lint-rules-rxjava2-lint/src/main/kotlin/com/vanniktech/lintrules/rxjava2/RxJava2DisposableAddAllCallDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.rxjava2
4 |
5 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
6 | import com.android.tools.lint.detector.api.Detector
7 | import com.android.tools.lint.detector.api.Implementation
8 | import com.android.tools.lint.detector.api.Issue
9 | import com.android.tools.lint.detector.api.JavaContext
10 | import com.android.tools.lint.detector.api.Scope.JAVA_FILE
11 | import com.android.tools.lint.detector.api.Severity.WARNING
12 | import com.intellij.psi.PsiMethod
13 | import org.jetbrains.uast.UCallExpression
14 | import java.util.EnumSet
15 |
16 | val ISSUE_DISPOSABLE_ADD_ALL_CALL = Issue.create(
17 | "RxJava2DisposableAddAllCall",
18 | "Marks usage of addAll() on CompositeDisposable.",
19 | "Instead of using addAll(), add() should be used separately for each Disposable.",
20 | CORRECTNESS,
21 | PRIORITY,
22 | WARNING,
23 | Implementation(RxJava2DisposableAddAllCallDetector::class.java, EnumSet.of(JAVA_FILE)),
24 | )
25 |
26 | class RxJava2DisposableAddAllCallDetector :
27 | Detector(),
28 | Detector.UastScanner {
29 | override fun getApplicableMethodNames() = listOf("addAll")
30 |
31 | override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
32 | if (context.evaluator.isMemberInClass(method, "io.reactivex.disposables.CompositeDisposable")) {
33 | context.report(ISSUE_DISPOSABLE_ADD_ALL_CALL, node, context.getNameLocation(node), "Calling `addAll` instead of add separately")
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lint-rules-kotlin-lint/src/main/kotlin/com/vanniktech/lintrules/kotlin/KotlinRequireNotNullUseMessageDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.kotlin
4 |
5 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
6 | import com.android.tools.lint.detector.api.Detector
7 | import com.android.tools.lint.detector.api.Implementation
8 | import com.android.tools.lint.detector.api.Issue
9 | import com.android.tools.lint.detector.api.JavaContext
10 | import com.android.tools.lint.detector.api.Scope.JAVA_FILE
11 | import com.android.tools.lint.detector.api.Severity.WARNING
12 | import com.intellij.psi.PsiMethod
13 | import org.jetbrains.uast.UCallExpression
14 | import java.util.EnumSet
15 |
16 | val ISSUE_KOTLIN_REQUIRE_NOT_NULL_USE_MESSAGE = Issue.create(
17 | "KotlinRequireNotNullUseMessage",
18 | "Marks usage of the requireNotNull method without lazy messages.",
19 | "The default generated message from requireNotNull often lacks context, hence it's best to provide a custom message.",
20 | CORRECTNESS,
21 | PRIORITY,
22 | WARNING,
23 | Implementation(KotlinRequireNotNullUseMessageDetector::class.java, EnumSet.of(JAVA_FILE)),
24 | )
25 |
26 | class KotlinRequireNotNullUseMessageDetector :
27 | Detector(),
28 | Detector.UastScanner {
29 | override fun getApplicableMethodNames() = listOf("requireNotNull")
30 |
31 | override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
32 | if (node.valueArgumentCount == 1) {
33 | context.report(ISSUE_KOTLIN_REQUIRE_NOT_NULL_USE_MESSAGE, node, context.getNameLocation(node), "Provide a message")
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/FormalGermanDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
6 | import com.android.tools.lint.detector.api.Implementation
7 | import com.android.tools.lint.detector.api.Issue
8 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
9 | import com.android.tools.lint.detector.api.Severity.WARNING
10 | import com.android.tools.lint.detector.api.XmlContext
11 | import org.w3c.dom.Node
12 |
13 | val ISSUE_FORMAL_GERMAN = Issue.create(
14 | "FormalGerman",
15 | "Marks strings which contain formal German words.",
16 | "Informal language should be used at all times.",
17 | CORRECTNESS,
18 | PRIORITY,
19 | WARNING,
20 | Implementation(FormalGermanDetector::class.java, RESOURCE_FILE_SCOPE),
21 | )
22 |
23 | class FormalGermanDetector : StringXmlDetector() {
24 | override fun checkText(context: XmlContext, node: Node, textNode: Node) {
25 | val items = FORMAL
26 | .flatMap { regex -> regex.findAll(textNode.nodeValue).map { it.range to it.value.trim() } }
27 | .distinctBy { it.first.first }
28 |
29 | items.forEach { (range, word) ->
30 | context.report(ISSUE_FORMAL_GERMAN, node, context.getLocation(textNode, range.first, range.last), "Formal language \"$word\" detected")
31 | }
32 | }
33 |
34 | companion object {
35 | val FORMAL = setOf(
36 | Regex("Sie\\W"),
37 | Regex("Ihr\\W"),
38 | Regex("Ihre\\W"),
39 | Regex("Ihrem\\W"),
40 | )
41 | const val CDATA = "
24 | val isStringResource = child.isTextNode() && TAG_STRING == element.localName
25 | val isStringArrayOrPlurals = child.isElementNode() && (TAG_STRING_ARRAY == element.localName || TAG_PLURALS == element.localName)
26 |
27 | if (isStringResource) {
28 | checkText(context, element, child)
29 | } else if (isStringArrayOrPlurals) {
30 | child.children()
31 | .filter { it.isTextNode() }
32 | .forEach { checkText(context, child, it) }
33 | }
34 | }
35 | }
36 |
37 | abstract fun checkText(context: XmlContext, node: Node, textNode: Node)
38 | }
39 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/ElementCollectReporter.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.detector.api.Context
6 | import com.android.tools.lint.detector.api.Issue
7 | import com.android.tools.lint.detector.api.LintFix
8 | import com.android.tools.lint.detector.api.Location
9 | import org.w3c.dom.Element
10 | import org.w3c.dom.Node
11 |
12 | class ElementCollectReporter(
13 | private val attributeToCollect: String,
14 | private val elementsToReport: MutableList> = mutableListOf(),
15 | ) : MutableCollection> by elementsToReport {
16 | private val items = ArrayList()
17 |
18 | fun collect(element: Element) {
19 | if (attributeToCollect == element.localName) {
20 | items.add(CollectedElement(element.getAttribute("name"), element.firstChild.nodeValue))
21 | }
22 | }
23 |
24 | @Suppress("Detekt.SpreadOperator") fun report(issue: Issue, context: Context, message: String) {
25 | elementsToReport
26 | .forEach { (node, location) ->
27 | val fixes = possibleSuggestions(node.nodeValue)
28 | .map { LintFix.create().replace().all().with(it).build() }
29 |
30 | val fix = if (fixes.isNotEmpty()) LintFix.create().group(*fixes.toTypedArray()) else null
31 | context.report(issue, location, message, fix)
32 | }
33 | }
34 |
35 | private fun possibleSuggestions(value: String) = items.filter { it.value == value }.map { "@$attributeToCollect/${it.name}" }
36 |
37 | private data class CollectedElement(
38 | val name: String,
39 | val value: String,
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/test/kotlin/com/vanniktech/lintrules/android/MissingXmlHeaderDetectorTest.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.checks.infrastructure.TestFiles.xml
6 | import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
7 | import com.android.tools.lint.checks.infrastructure.TestMode
8 | import org.junit.Test
9 |
10 | class MissingXmlHeaderDetectorTest {
11 | @Test fun hasXmlHeader() {
12 | lint()
13 | .files(
14 | xml(
15 | "res/values/strings.xml",
16 | """
17 |
18 |
19 | """,
20 | ).indented(),
21 | )
22 | .issues(ISSUE_MISSING_XML_HEADER)
23 | .run()
24 | .expectClean()
25 | }
26 |
27 | @Test fun missingHeader() {
28 | lint()
29 | .files(
30 | xml(
31 | "res/values/strings.xml",
32 | """
33 |
34 | """,
35 | ).indented(),
36 | )
37 | .issues(ISSUE_MISSING_XML_HEADER)
38 | .skipTestModes(TestMode.SUPPRESSIBLE)
39 | .run()
40 | .expect(
41 | """
42 | |res/values/strings.xml:1: Warning: Missing an xml header [MissingXmlHeader]
43 | |
44 | |~~~~~~~~~~~~
45 | |0 errors, 1 warnings
46 | """.trimMargin(),
47 | )
48 | .expectFixDiffs(
49 | """
50 | |Fix for res/values/strings.xml line 0: Add xml header:
51 | |@@ -1 +1
52 | |+
53 | """.trimMargin(),
54 | )
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/WrongLayoutNameDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
6 | import com.android.tools.lint.detector.api.Implementation
7 | import com.android.tools.lint.detector.api.Issue
8 | import com.android.tools.lint.detector.api.LayoutDetector
9 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
10 | import com.android.tools.lint.detector.api.Severity.WARNING
11 | import com.android.tools.lint.detector.api.XmlContext
12 | import org.w3c.dom.Document
13 |
14 | private val allowedPrefixes = listOf("activity_", "view_", "fragment_", "dialog_", "bottom_sheet_", "adapter_item_", "divider_", "space_", "popup_window_")
15 |
16 | val ISSUE_WRONG_LAYOUT_NAME = Issue.create(
17 | "WrongLayoutName",
18 | "Layout names should be prefixed accordingly.",
19 | "The layout file name should be prefixed with one of the following: ${allowedPrefixes.joinToString()}. This will improve consistency in your code base as well as enforce a certain structure.",
20 | CORRECTNESS,
21 | PRIORITY,
22 | WARNING,
23 | Implementation(WrongLayoutNameDetector::class.java, RESOURCE_FILE_SCOPE),
24 | )
25 |
26 | class WrongLayoutNameDetector : LayoutDetector() {
27 | override fun visitDocument(context: XmlContext, document: Document) {
28 | val modified = fileNameSuggestions(allowedPrefixes, context)
29 |
30 | if (modified != null) {
31 | context.report(ISSUE_WRONG_LAYOUT_NAME, document, context.getLocation(document), "Layout does not start with one of the following prefixes: ${modified.joinToString()}")
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/DefaultLayoutAttributeDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.SdkConstants.ATTR_TEXT_STYLE
6 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
7 | import com.android.tools.lint.detector.api.Implementation
8 | import com.android.tools.lint.detector.api.Issue
9 | import com.android.tools.lint.detector.api.LayoutDetector
10 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
11 | import com.android.tools.lint.detector.api.Severity.WARNING
12 | import com.android.tools.lint.detector.api.XmlContext
13 | import org.w3c.dom.Attr
14 |
15 | val ISSUE_DEFAULT_LAYOUT_ATTRIBUTE = Issue.create(
16 | "DefaultLayoutAttribute",
17 | "Flags default layout values.",
18 | "Flags default layout values that are not needed. One for instance is the textStyle=\"normal\" that can be just removed.",
19 | CORRECTNESS,
20 | PRIORITY,
21 | WARNING,
22 | Implementation(DefaultLayoutAttributeDetector::class.java, RESOURCE_FILE_SCOPE),
23 | )
24 |
25 | class DefaultLayoutAttributeDetector : LayoutDetector() {
26 | override fun getApplicableAttributes() = listOf(ATTR_TEXT_STYLE)
27 |
28 | override fun visitAttribute(context: XmlContext, attribute: Attr) {
29 | if ("normal" == attribute.value) {
30 | val fix = fix()
31 | .unset(attribute.namespaceURI, attribute.localName)
32 | .name("Remove")
33 | .autoFix()
34 | .build()
35 |
36 | context.report(ISSUE_DEFAULT_LAYOUT_ATTRIBUTE, attribute, context.getValueLocation(attribute), "This is the default and hence you don't need to specify it", fix)
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/JcenterDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
6 | import com.android.tools.lint.detector.api.Detector
7 | import com.android.tools.lint.detector.api.GradleContext
8 | import com.android.tools.lint.detector.api.Implementation
9 | import com.android.tools.lint.detector.api.Issue
10 | import com.android.tools.lint.detector.api.Scope.GRADLE_FILE
11 | import com.android.tools.lint.detector.api.Severity.WARNING
12 | import java.util.EnumSet
13 |
14 | val ISSUE_JCENTER = Issue.create(
15 | "JCenter",
16 | "Marks usage of the jcenter() repository.",
17 | "JCenter has gotten less and less reliable and it's best to avoid if possible. This check will flag usages of jcenter() in your gradle files.",
18 | CORRECTNESS,
19 | PRIORITY,
20 | WARNING,
21 | Implementation(JcenterDetector::class.java, EnumSet.of(GRADLE_FILE)),
22 | )
23 |
24 | class JcenterDetector :
25 | Detector(),
26 | Detector.GradleScanner {
27 | override fun checkMethodCall(
28 | context: GradleContext,
29 | statement: String,
30 | parent: String?,
31 | parentParent: String?,
32 | namedArguments: Map,
33 | unnamedArguments: List,
34 | cookie: Any,
35 | ) {
36 | if (statement == "jcenter") {
37 | val fix = fix()
38 | .replace()
39 | .text(statement)
40 | .with("mavenCentral")
41 | .name("Replace with mavenCentral()")
42 | .autoFix()
43 | .build()
44 | context.report(ISSUE_JCENTER, cookie, context.getLocation(cookie), "Don't use `jcenter()`", fix)
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/MissingXmlHeaderDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
6 | import com.android.tools.lint.detector.api.Implementation
7 | import com.android.tools.lint.detector.api.Issue
8 | import com.android.tools.lint.detector.api.Location
9 | import com.android.tools.lint.detector.api.ResourceXmlDetector
10 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
11 | import com.android.tools.lint.detector.api.Severity.WARNING
12 | import com.android.tools.lint.detector.api.XmlContext
13 | import org.w3c.dom.Document
14 |
15 | val ISSUE_MISSING_XML_HEADER = Issue.create(
16 | "MissingXmlHeader",
17 | "Flags xml files that don't have a header.",
18 | "An xml file should always have the xml header to declare that it is an xml file despite the file ending.",
19 | CORRECTNESS,
20 | PRIORITY,
21 | WARNING,
22 | Implementation(MissingXmlHeaderDetector::class.java, RESOURCE_FILE_SCOPE),
23 | )
24 |
25 | class MissingXmlHeaderDetector : ResourceXmlDetector() {
26 | override fun visitDocument(context: XmlContext, document: Document) {
27 | val content = context.client.readFile(context.file)
28 |
29 | if (!content.startsWith("\n$content")
35 | .autoFix()
36 | .build()
37 |
38 | context.report(ISSUE_MISSING_XML_HEADER, document, Location.create(context.file, content, 0, content.length), "Missing an xml header", fix)
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/WrongViewIdFormatDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.SdkConstants.ATTR_ID
6 | import com.android.resources.ResourceUrl
7 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
8 | import com.android.tools.lint.detector.api.Implementation
9 | import com.android.tools.lint.detector.api.Issue
10 | import com.android.tools.lint.detector.api.LayoutDetector
11 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
12 | import com.android.tools.lint.detector.api.Severity.WARNING
13 | import com.android.tools.lint.detector.api.XmlContext
14 | import org.w3c.dom.Attr
15 |
16 | val ISSUE_WRONG_VIEW_ID_FORMAT = Issue.create(
17 | "WrongViewIdFormat",
18 | "Flag view ids that are not in lowerCamelCase Format.",
19 | "View ids should be in lowerCamelCase format. This has the benefit of saving an unnecessary underscore and also just looks nicer.",
20 | CORRECTNESS,
21 | PRIORITY,
22 | WARNING,
23 | Implementation(WrongViewIdFormatDetector::class.java, RESOURCE_FILE_SCOPE),
24 | )
25 |
26 | class WrongViewIdFormatDetector : LayoutDetector() {
27 | override fun getApplicableAttributes() = listOf(ATTR_ID)
28 |
29 | override fun visitAttribute(context: XmlContext, attribute: Attr) {
30 | val name = ResourceUrl.parse(attribute.value)?.name
31 |
32 | if (name != null && !name.isLowerCamelCase()) {
33 | val fix = fix().replace()
34 | .name("Convert to lowerCamelCase")
35 | .text(attribute.value)
36 | .with(attribute.value.idToLowerCamelCase())
37 | .autoFix()
38 | .build()
39 |
40 | context.report(ISSUE_WRONG_VIEW_ID_FORMAT, attribute, context.getValueLocation(attribute), "Id is not in `lowerCamelCaseFormat`", fix)
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/ConstraintLayoutToolsEditorAttributeDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
6 | import com.android.tools.lint.detector.api.Implementation
7 | import com.android.tools.lint.detector.api.Issue
8 | import com.android.tools.lint.detector.api.LayoutDetector
9 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
10 | import com.android.tools.lint.detector.api.Severity.WARNING
11 | import com.android.tools.lint.detector.api.XmlContext
12 | import org.w3c.dom.Attr
13 |
14 | val ISSUE_CONSTRAINT_LAYOUT_TOOLS_EDITOR_ATTRIBUTE_DETECTOR = Issue.create(
15 | "ConstraintLayoutToolsEditorAttribute",
16 | "Flags tools:layout_editor xml properties.",
17 | "The tools:layout_editor xml properties are only used for previewing and won't be used in your APK hence they're unnecessary and just add overhead.",
18 | CORRECTNESS,
19 | PRIORITY,
20 | WARNING,
21 | Implementation(ConstraintLayoutToolsEditorAttributeDetector::class.java, RESOURCE_FILE_SCOPE),
22 | )
23 |
24 | class ConstraintLayoutToolsEditorAttributeDetector : LayoutDetector() {
25 | override fun getApplicableAttributes() = ALL
26 |
27 | override fun visitAttribute(context: XmlContext, attribute: Attr) {
28 | val isLayoutEditorAttribute = attribute.localName?.startsWith("layout_editor_") ?: false
29 |
30 | if (isLayoutEditorAttribute && attribute.hasToolsNamespace()) {
31 | val fix = fix()
32 | .unset(attribute.namespaceURI, attribute.localName)
33 | .name("Remove")
34 | .autoFix()
35 | .build()
36 |
37 | context.report(ISSUE_CONSTRAINT_LAYOUT_TOOLS_EDITOR_ATTRIBUTE_DETECTOR, attribute, context.getNameLocation(attribute), "Don't use ${attribute.name}", fix)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/test/kotlin/com/vanniktech/lintrules/android/IssueRegistryTest.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.client.api.LintClient
6 | import com.android.tools.lint.detector.api.TextFormat.RAW
7 | import org.junit.After
8 | import org.junit.Assert.assertTrue
9 | import org.junit.Before
10 | import org.junit.Test
11 | import java.io.File
12 |
13 | class IssueRegistryTest {
14 | @Before fun setUp() {
15 | LintClient.clientName = "Test"
16 | }
17 |
18 | @After fun tearDown() {
19 | LintClient.resetClientName()
20 | }
21 |
22 | @Test fun everyBriefDescriptionIsASentence() {
23 | IssueRegistry().issues
24 | .map { it.getBriefDescription(RAW) }
25 | .forEach { assertTrue("$it is not a sentence", it.first().isUpperCase() && it.last() == '.' && it == it.trim()) }
26 | }
27 |
28 | @Test fun everyExplanationConsistsOfSentences() {
29 | IssueRegistry().issues
30 | .map { it.getExplanation(RAW) }
31 | .forEach { assertTrue("$it is not a sentence", it.first().isUpperCase() && it.last() == '.' && it == it.trim()) }
32 | }
33 |
34 | @Test fun idsDoNotHaveDetector() {
35 | IssueRegistry().issues
36 | .map { it.id }
37 | .forEach { assertTrue("$it is containing Detector", !it.contains("Detector")) }
38 | }
39 |
40 | @Test fun readmeContent() {
41 | val output = IssueRegistry().issues
42 | .sortedBy { it.id }
43 | .joinToString(separator = "\n") { "- **${it.id}** - ${it.getExplanation(RAW)}" }
44 |
45 | requireNotNull(
46 | File(requireNotNull(IssueRegistryTest::class.java.classLoader).getResource(".").file)
47 | .parentFile
48 | ?.parentFile
49 | ?.parentFile
50 | ?.parentFile
51 | ?.parentFile
52 | ?.resolve("lint-rules-android.md"),
53 | )
54 | .writeText(output)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/InvalidAccessibilityDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.SdkConstants
6 | import com.android.SdkConstants.ATTR_CONTENT_DESCRIPTION
7 | import com.android.SdkConstants.ATTR_IMPORTANT_FOR_ACCESSIBILITY
8 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
9 | import com.android.tools.lint.detector.api.Implementation
10 | import com.android.tools.lint.detector.api.Issue
11 | import com.android.tools.lint.detector.api.LayoutDetector
12 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
13 | import com.android.tools.lint.detector.api.Severity.WARNING
14 | import com.android.tools.lint.detector.api.XmlContext
15 | import org.w3c.dom.Attr
16 |
17 | val ISSUE_INVALID_ACCESSIBILITY = Issue.create(
18 | "InvalidAccessibility",
19 | "Marks invalid accessibility usages.",
20 | "Marks usages of invalid accessibility and suggests corrections.",
21 | CORRECTNESS,
22 | PRIORITY,
23 | WARNING,
24 | Implementation(InvalidAccessibilityDetector::class.java, RESOURCE_FILE_SCOPE),
25 | )
26 |
27 | class InvalidAccessibilityDetector : LayoutDetector() {
28 | override fun getApplicableAttributes() = listOf(ATTR_CONTENT_DESCRIPTION)
29 |
30 | override fun visitAttribute(
31 | context: XmlContext,
32 | attribute: Attr,
33 | ) {
34 | if (attribute.value == "@null") {
35 | val fix = fix().name("Change").composite(
36 | fix().set(SdkConstants.ANDROID_URI, ATTR_IMPORTANT_FOR_ACCESSIBILITY, "no").build(),
37 | fix().unset(attribute.namespaceURI, attribute.localName).build(),
38 | ).autoFix()
39 |
40 | context.report(ISSUE_INVALID_ACCESSIBILITY, attribute, context.getValueLocation(attribute), "Either set a proper accessibility text or use $ATTR_IMPORTANT_FOR_ACCESSIBILITY", fix)
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lint-rules-rxjava2-lint/src/main/kotlin/com/vanniktech/lintrules/rxjava2/RxJava2DisposableDisposeCallDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.rxjava2
4 |
5 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
6 | import com.android.tools.lint.detector.api.Detector
7 | import com.android.tools.lint.detector.api.Implementation
8 | import com.android.tools.lint.detector.api.Issue
9 | import com.android.tools.lint.detector.api.JavaContext
10 | import com.android.tools.lint.detector.api.Scope.JAVA_FILE
11 | import com.android.tools.lint.detector.api.Severity.WARNING
12 | import com.intellij.psi.PsiMethod
13 | import org.jetbrains.uast.UCallExpression
14 | import java.util.EnumSet
15 |
16 | val ISSUE_DISPOSABLE_DISPOSE_CALL = Issue.create(
17 | "RxJava2DisposableDisposeCall",
18 | "Marks usage of dispose() on CompositeDisposable.",
19 | "Instead of using dispose(), clear() should be used. Calling clear will result in a CompositeDisposable that can be used further to add more Disposables. When using dispose() this is not the case.",
20 | CORRECTNESS,
21 | PRIORITY,
22 | WARNING,
23 | Implementation(RxJava2DisposableDisposeCallDetector::class.java, EnumSet.of(JAVA_FILE)),
24 | )
25 |
26 | class RxJava2DisposableDisposeCallDetector :
27 | Detector(),
28 | Detector.UastScanner {
29 | override fun getApplicableMethodNames() = listOf("dispose")
30 |
31 | override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
32 | if (context.evaluator.isMemberInClass(method, "io.reactivex.disposables.CompositeDisposable")) {
33 | val fix = fix()
34 | .name("Fix it")
35 | .replace()
36 | .text("dispose")
37 | .with("clear")
38 | .autoFix()
39 | .build()
40 |
41 | context.report(ISSUE_DISPOSABLE_DISPOSE_CALL, node, context.getNameLocation(node), "Calling `dispose` instead of `clear`", fix)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/WrongDrawableNameDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.resources.ResourceFolderType
6 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
7 | import com.android.tools.lint.detector.api.Implementation
8 | import com.android.tools.lint.detector.api.Issue
9 | import com.android.tools.lint.detector.api.ResourceXmlDetector
10 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
11 | import com.android.tools.lint.detector.api.Severity.WARNING
12 | import com.android.tools.lint.detector.api.XmlContext
13 | import org.w3c.dom.Document
14 |
15 | private val allowedPrefixes = listOf(
16 | "animated_selector",
17 | "animated_vector_",
18 | "background_",
19 | "ic_",
20 | "img_",
21 | "notification_icon_",
22 | "ripple_",
23 | "selector_",
24 | "shape_",
25 | "vector_",
26 | )
27 |
28 | val ISSUE_WRONG_DRAWABLE_NAME = Issue.create(
29 | "WrongDrawableName",
30 | "Drawable names should be prefixed accordingly.",
31 | "The drawable file name should be prefixed with one of the following: ${allowedPrefixes.joinToString()}. This will improve consistency in your code base as well as enforce a certain structure.",
32 | CORRECTNESS,
33 | PRIORITY,
34 | WARNING,
35 | Implementation(WrongDrawableNameDetector::class.java, RESOURCE_FILE_SCOPE),
36 | )
37 |
38 | class WrongDrawableNameDetector : ResourceXmlDetector() {
39 | override fun appliesTo(folderType: ResourceFolderType) = folderType == ResourceFolderType.DRAWABLE
40 |
41 | override fun visitDocument(context: XmlContext, document: Document) {
42 | val modified = fileNameSuggestions(allowedPrefixes, context)
43 |
44 | if (modified != null) {
45 | context.report(ISSUE_WRONG_DRAWABLE_NAME, document, context.getLocation(document), "Drawable does not start with one of the following prefixes: ${modified.joinToString()}")
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/ColorCasingDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.resources.ResourceFolderType
6 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
7 | import com.android.tools.lint.detector.api.Implementation
8 | import com.android.tools.lint.detector.api.Issue
9 | import com.android.tools.lint.detector.api.ResourceXmlDetector
10 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
11 | import com.android.tools.lint.detector.api.Severity.WARNING
12 | import com.android.tools.lint.detector.api.XmlContext
13 | import org.w3c.dom.Attr
14 | import org.w3c.dom.Element
15 |
16 | val ISSUE_COLOR_CASING = Issue.create(
17 | "ColorCasing",
18 | "Raw colors should be defined with uppercase letters.",
19 | "Colors should have uppercase letters. #FF0099 is valid while #ff0099 isn't since the ff should be written in uppercase.",
20 | CORRECTNESS,
21 | PRIORITY,
22 | WARNING,
23 | Implementation(ColorCasingDetector::class.java, RESOURCE_FILE_SCOPE),
24 | )
25 |
26 | class ColorCasingDetector : ResourceXmlDetector() {
27 | override fun appliesTo(folderType: ResourceFolderType) = true
28 |
29 | override fun getApplicableElements() = ALL
30 |
31 | override fun visitElement(context: XmlContext, element: Element) {
32 | element.attributes()
33 | .filter { it.nodeValue.matches(COLOR_REGEX) }
34 | .filter { it.nodeValue.any { it.isLowerCase() } }
35 | .forEach {
36 | val fix = fix()
37 | .name("Convert to uppercase")
38 | .replace()
39 | .text(it.nodeValue)
40 | .with(it.nodeValue.uppercase())
41 | .autoFix()
42 | .build()
43 |
44 | context.report(ISSUE_COLOR_CASING, it, context.getValueLocation(it as Attr), "Should be using uppercase letters", fix)
45 | }
46 | }
47 |
48 | companion object {
49 | val COLOR_REGEX = Regex("#[a-fA-F\\d]{3,8}")
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/UnusedMergeAttributesDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.SdkConstants.TOOLS_URI
6 | import com.android.SdkConstants.VIEW_MERGE
7 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
8 | import com.android.tools.lint.detector.api.Implementation
9 | import com.android.tools.lint.detector.api.Issue
10 | import com.android.tools.lint.detector.api.LayoutDetector
11 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
12 | import com.android.tools.lint.detector.api.Severity.WARNING
13 | import com.android.tools.lint.detector.api.XmlContext
14 | import org.w3c.dom.Element
15 |
16 | val ISSUE_UNUSED_MERGE_ATTRIBUTES = Issue.create(
17 | "UnusedMergeAttributes",
18 | "Flags android and app attributes that are used on a attribute for custom Views.",
19 | "Adding android, app and other attributes to won't be used by the system for custom views and hence can lead to errors.",
20 | CORRECTNESS,
21 | PRIORITY,
22 | WARNING,
23 | Implementation(UnusedMergeAttributesDetector::class.java, RESOURCE_FILE_SCOPE),
24 | )
25 |
26 | class UnusedMergeAttributesDetector : LayoutDetector() {
27 | override fun getApplicableElements() = listOf(VIEW_MERGE)
28 |
29 | override fun visitElement(context: XmlContext, element: Element) {
30 | val hasParentTag = element.parentTag().isNotEmpty()
31 |
32 | if (hasParentTag) {
33 | element.attributes()
34 | .filterNot { it.hasToolsNamespace() }
35 | .filterNot { it.prefix == "xmlns" }
36 | .forEach {
37 | val fix = fix().name("Change to tools").composite(
38 | fix().set(TOOLS_URI, it.localName, it.nodeValue).build(),
39 | fix().unset(it.namespaceURI, it.localName).build(),
40 | ).autoFix()
41 |
42 | context.report(ISSUE_UNUSED_MERGE_ATTRIBUTES, it, context.getLocation(it), "Attribute won't be used", fix)
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/SuperfluousNameSpaceDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.SdkConstants.AAPT_URI
6 | import com.android.SdkConstants.ANDROID_URI
7 | import com.android.SdkConstants.AUTO_URI
8 | import com.android.SdkConstants.TOOLS_URI
9 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
10 | import com.android.tools.lint.detector.api.Implementation
11 | import com.android.tools.lint.detector.api.Issue
12 | import com.android.tools.lint.detector.api.LayoutDetector
13 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
14 | import com.android.tools.lint.detector.api.Severity.WARNING
15 | import com.android.tools.lint.detector.api.XmlContext
16 | import org.w3c.dom.Element
17 |
18 | private val possibleUris = setOf(ANDROID_URI, TOOLS_URI, AUTO_URI, AAPT_URI)
19 |
20 | val ISSUE_SUPERFLUOUS_NAME_SPACE = Issue.create(
21 | "SuperfluousNameSpace",
22 | "Flags namespaces that are already declared.",
23 | "Re-declaring a namespace is unnecessary and hence can be just removed.",
24 | CORRECTNESS,
25 | PRIORITY,
26 | WARNING,
27 | Implementation(SuperfluousNameSpaceDetector::class.java, RESOURCE_FILE_SCOPE),
28 | )
29 |
30 | class SuperfluousNameSpaceDetector : LayoutDetector() {
31 | override fun getApplicableElements() = ALL
32 |
33 | override fun visitElement(context: XmlContext, element: Element) {
34 | if (element.parentNode.parentNode != null) {
35 | element.attributes()
36 | .filter { attribute -> possibleUris.any { attribute.toString().contains(it) } }
37 | .forEach {
38 | val fix = fix()
39 | .name("Remove namespace")
40 | .replace()
41 | .range(context.getLocation(it))
42 | .all()
43 | .build()
44 |
45 | context.report(ISSUE_SUPERFLUOUS_NAME_SPACE, it, context.getLocation(it), "This name space is already declared and hence not needed", fix)
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/MatchingViewIdDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.SdkConstants.ATTR_ID
6 | import com.android.resources.ResourceUrl
7 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
8 | import com.android.tools.lint.detector.api.Implementation
9 | import com.android.tools.lint.detector.api.Issue
10 | import com.android.tools.lint.detector.api.LayoutDetector
11 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
12 | import com.android.tools.lint.detector.api.Severity.WARNING
13 | import com.android.tools.lint.detector.api.XmlContext
14 | import org.w3c.dom.Attr
15 |
16 | val ISSUE_MATCHING_VIEW_ID = Issue.create(
17 | "MatchingViewId",
18 | "Flags view ids that don't match with the file name.",
19 | "When the layout file is named activity_home all of the containing ids should be prefixed with activityHome to avoid ambiguity between different layout files across different views.",
20 | CORRECTNESS,
21 | PRIORITY,
22 | WARNING,
23 | Implementation(MatchingViewIdDetector::class.java, RESOURCE_FILE_SCOPE),
24 | )
25 |
26 | class MatchingViewIdDetector : LayoutDetector() {
27 | override fun getApplicableAttributes() = listOf(ATTR_ID)
28 |
29 | override fun visitAttribute(context: XmlContext, attribute: Attr) {
30 | val id = ResourceUrl.parse(attribute.value)?.name ?: return
31 | val fixer = MatchingIdFixer(context, id)
32 | val isAndroidId = attribute.value.startsWith("@android:id/")
33 | val isUsingViewBinding = context.project.buildVariant?.buildFeatures?.viewBinding == true
34 |
35 | if (fixer.needsFix() && !isAndroidId && !isUsingViewBinding) {
36 | val fix = fix()
37 | .replace()
38 | .text(id)
39 | .with(fixer.fixedId())
40 | .autoFix()
41 | .build()
42 |
43 | context.report(ISSUE_MATCHING_VIEW_ID, attribute, context.getValueLocation(attribute), "Id should start with: ${fixer.expectedPrefix}", fix)
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/main/kotlin/com/vanniktech/lintrules/android/XmlSpacingDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.ide.common.blame.SourcePosition
6 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
7 | import com.android.tools.lint.detector.api.Implementation
8 | import com.android.tools.lint.detector.api.Issue
9 | import com.android.tools.lint.detector.api.Location
10 | import com.android.tools.lint.detector.api.ResourceXmlDetector
11 | import com.android.tools.lint.detector.api.Scope.Companion.RESOURCE_FILE_SCOPE
12 | import com.android.tools.lint.detector.api.Severity.WARNING
13 | import com.android.tools.lint.detector.api.XmlContext
14 | import org.w3c.dom.Document
15 |
16 | val ISSUE_XML_SPACING = Issue.create(
17 | "XmlSpacing",
18 | "XML files should not contain any new lines.",
19 | "Having newlines in xml files just adds noise and should be avoided. The only exception is the new lint at the end of the file.",
20 | CORRECTNESS,
21 | PRIORITY,
22 | WARNING,
23 | Implementation(XmlSpacingDetector::class.java, RESOURCE_FILE_SCOPE),
24 | )
25 |
26 | class XmlSpacingDetector : ResourceXmlDetector() {
27 | override fun visitDocument(context: XmlContext, document: Document) {
28 | val contents = context.client.readFile(context.file).toString().split("\n")
29 |
30 | contents
31 | .withIndex()
32 | .windowed(2)
33 | .filter { it[0].value.isBlank() && it.getOrNull(1)?.value?.trim()?.startsWith("
65 |
66 | """,
67 | ).indented(),
68 | )
69 | .issues(ISSUE_XML_SPACING)
70 | .run()
71 | .expectClean()
72 | }
73 |
74 | @Test fun layoutXmlFileWithNewLines() {
75 | lint()
76 | .files(
77 | xml(
78 | "res/layout/activity_home.xml",
79 | """
80 |
81 |
82 |
83 |
88 |
89 |
90 | """,
91 | ).indented(),
92 | )
93 | .issues(ISSUE_XML_SPACING)
94 | .skipTestModes(TestMode.SUPPRESSIBLE)
95 | .run()
96 | .expect(
97 | """
98 | |res/layout/activity_home.xml:1: Warning: Unnecessary new line at line 1 [XmlSpacing]
99 | |
100 | |^
101 | |res/layout/activity_home.xml:3: Warning: Unnecessary new line at line 3 [XmlSpacing]
102 | |
103 | |^
104 | |res/layout/activity_home.xml:7: Warning: Unnecessary new line at line 7 [XmlSpacing]
105 | |
106 | |^
107 | |res/layout/activity_home.xml:9: Warning: Unnecessary new line at line 9 [XmlSpacing]
108 | |
109 | |^
110 | |0 errors, 4 warnings
111 | """.trimMargin(),
112 | )
113 | .expectFixDiffs(
114 | """
115 | |Fix for res/layout/activity_home.xml line 0: Remove new line:
116 | |@@ -1 +1
117 | |-
118 | |Fix for res/layout/activity_home.xml line 2: Remove new line:
119 | |@@ -1 +1
120 | |-
121 | |Fix for res/layout/activity_home.xml line 6: Remove new line:
122 | |@@ -1 +1
123 | |-
124 | |Fix for res/layout/activity_home.xml line 8: Remove new line:
125 | |@@ -1 +1
126 | |-
127 | """.trimMargin(),
128 | )
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/test/kotlin/com/vanniktech/lintrules/android/WrongTestMethodNameDetectorTest.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.checks.infrastructure.TestFiles.java
6 | import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
7 | import org.junit.Test
8 |
9 | class WrongTestMethodNameDetectorTest {
10 | @Test fun methodStartingWithTest() {
11 | lint()
12 | .files(
13 | java(
14 | """
15 | package foo;
16 |
17 | public class Something {
18 | public void testThis() { }
19 | }
20 | """,
21 | ).indented(),
22 | )
23 | .issues(ISSUE_WRONG_TEST_METHOD_NAME)
24 | .run()
25 | .expectClean()
26 | }
27 |
28 | @Test fun methodStartingWithTestAndSomethingAnnotation() {
29 | lint()
30 | .files(
31 | stubAnnotationSomething,
32 | java(
33 | """
34 | package foo;
35 |
36 | import my.custom.Something;
37 |
38 | public class MyTest {
39 | @Something public void test() { }
40 | }
41 | """,
42 | ).indented(),
43 | )
44 | .issues(ISSUE_WRONG_TEST_METHOD_NAME)
45 | .run()
46 | .expectClean()
47 | }
48 |
49 | @Test fun methodNotStartingWithTestAndTestAnnotation() {
50 | lint()
51 | .files(
52 | stubJUnitTest,
53 | java(
54 | """
55 | package foo;
56 |
57 | import org.junit.Test;
58 |
59 | public class MyTest {
60 | @Test public void myTest() { }
61 | }
62 | """,
63 | ).indented(),
64 | )
65 | .issues(ISSUE_WRONG_TEST_METHOD_NAME)
66 | .run()
67 | .expectClean()
68 | }
69 |
70 | @Test fun methodStartingWithTestAndJUnitTestAnnotation() {
71 | lint()
72 | .files(
73 | stubJUnitTest,
74 | java(
75 | """
76 | package foo;
77 |
78 | import org.junit.Test;
79 |
80 | public class MyTest {
81 | @Test public void testSomething() { }
82 | }
83 | """,
84 | ).indented(),
85 | )
86 | .issues(ISSUE_WRONG_TEST_METHOD_NAME)
87 | .run()
88 | .expect(
89 | """
90 | |src/foo/MyTest.java:6: Warning: Test method starts with test [WrongTestMethodName]
91 | | @Test public void testSomething() { }
92 | | ~~~~~~~~~~~~~
93 | |0 errors, 1 warnings
94 | """.trimMargin(),
95 | )
96 | .expectFixDiffs(
97 | """
98 | |Fix for src/foo/MyTest.java line 5: Remove test prefix:
99 | |@@ -6 +6
100 | |- @Test public void testSomething() { }
101 | |+ @Test public void something() { }
102 | """.trimMargin(),
103 | )
104 | }
105 |
106 | @Test fun methodStartingWithTestAndCustomTestAnnotation() {
107 | lint()
108 | .files(
109 | stubAnnotationTest,
110 | java(
111 | """
112 | package foo;
113 |
114 | import my.custom.Test;
115 |
116 | public class MyTest {
117 | @Test public void testSomething() { }
118 | }
119 | """,
120 | ).indented(),
121 | )
122 | .issues(ISSUE_WRONG_TEST_METHOD_NAME)
123 | .run()
124 | .expect(
125 | """
126 | |src/foo/MyTest.java:6: Warning: Test method starts with test [WrongTestMethodName]
127 | | @Test public void testSomething() { }
128 | | ~~~~~~~~~~~~~
129 | |0 errors, 1 warnings
130 | """.trimMargin(),
131 | )
132 | .expectFixDiffs(
133 | """
134 | |Fix for src/foo/MyTest.java line 5: Remove test prefix:
135 | |@@ -6 +6
136 | |- @Test public void testSomething() { }
137 | |+ @Test public void something() { }
138 | """.trimMargin(),
139 | )
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/test/kotlin/com/vanniktech/lintrules/android/ImplicitStringPlaceholderDetectorTest.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.checks.infrastructure.TestFiles.xml
6 | import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
7 | import org.junit.Test
8 |
9 | class ImplicitStringPlaceholderDetectorTest {
10 | @Test fun valid() {
11 | lint()
12 | .files(
13 | xml(
14 | "res/values/strings.xml",
15 | """
16 |
17 | My string
18 | Hello %1$s
19 |
20 | """,
21 | ).indented(),
22 | )
23 | .issues(ISSUE_IMPLICIT_STRING_PLACEHOLDER)
24 | .run()
25 | .expectClean()
26 | }
27 |
28 | @Test fun invalid() {
29 | lint()
30 | .files(
31 | xml(
32 | "res/values/strings.xml",
33 | """
34 |
35 | Hello %s
36 |
37 | """,
38 | ).indented(),
39 | )
40 | .issues(ISSUE_IMPLICIT_STRING_PLACEHOLDER)
41 | .run()
42 | .expect(
43 | """
44 | res/values/strings.xml:2: Warning: Implicit placeholder [ImplicitStringPlaceholder]
45 | Hello %s
46 | ~~
47 | 0 errors, 1 warnings
48 | """.trimIndent(),
49 | )
50 | .expectFixDiffs(
51 | """
52 | Autofix for res/values/strings.xml line 2: Fix %s with %1$s:
53 | @@ -2 +2
54 | - Hello %s
55 | + Hello %1$s
56 | """.trimIndent(),
57 | )
58 | }
59 |
60 | @Test fun multiple() {
61 | lint()
62 | .files(
63 | xml(
64 | "res/values/strings.xml",
65 | """
66 |
67 | Hello %s, %d days ago, what did you like %s?
68 |
69 | """,
70 | ).indented(),
71 | )
72 | .issues(ISSUE_IMPLICIT_STRING_PLACEHOLDER)
73 | .run()
74 | .expect(
75 | """
76 | res/values/strings.xml:2: Warning: Implicit placeholder [ImplicitStringPlaceholder]
77 | Hello %s, %d days ago, what did you like %s?
78 | ~~
79 | res/values/strings.xml:2: Warning: Implicit placeholder [ImplicitStringPlaceholder]
80 | Hello %s, %d days ago, what did you like %s?
81 | ~~
82 | res/values/strings.xml:2: Warning: Implicit placeholder [ImplicitStringPlaceholder]
83 | Hello %s, %d days ago, what did you like %s?
84 | ~~
85 | 0 errors, 3 warnings
86 | """.trimIndent(),
87 | )
88 | .expectFixDiffs(
89 | """
90 | Autofix for res/values/strings.xml line 2: Fix %s with %1$s:
91 | @@ -2 +2
92 | - Hello %s, %d days ago, what did you like %s?
93 | + Hello %1$s, %d days ago, what did you like %s?
94 | Autofix for res/values/strings.xml line 2: Fix %d with %2$d:
95 | @@ -2 +2
96 | - Hello %s, %d days ago, what did you like %s?
97 | + Hello %s, %2$d days ago, what did you like %s?
98 | Autofix for res/values/strings.xml line 2: Fix %s with %3$s:
99 | @@ -2 +2
100 | - Hello %s, %d days ago, what did you like %s?
101 | + Hello %s, %d days ago, what did you like %3$s?
102 | """.trimIndent(),
103 | )
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/lint-rules-android-lint/src/test/kotlin/com/vanniktech/lintrules/android/UnsupportedLayoutAttributeDetectorTest.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.android
4 |
5 | import com.android.tools.lint.checks.infrastructure.TestFiles.xml
6 | import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
7 | import org.junit.Test
8 |
9 | class UnsupportedLayoutAttributeDetectorTest {
10 | @Test fun orientationInRelativeLayout() {
11 | lint()
12 | .files(
13 | xml(
14 | "res/layout/activity_home.xml",
15 | """
16 |
19 | """,
20 | ).indented(),
21 | )
22 | .issues(ISSUE_UNSUPPORTED_LAYOUT_ATTRIBUTE)
23 | .run()
24 | .expect(
25 | """
26 | |res/layout/activity_home.xml:3: Error: orientation is not allowed in RelativeLayout [UnsupportedLayoutAttribute]
27 | | android:orientation="vertical"/>
28 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
29 | |1 errors, 0 warnings
30 | """.trimMargin(),
31 | )
32 | .expectFixDiffs(
33 | """
34 | |Fix for res/layout/activity_home.xml line 2: Remove unnecessary attribute:
35 | |@@ -2 +2
36 | |-
38 | |+
39 | """.trimMargin(),
40 | )
41 | }
42 |
43 | @Test fun orientationInScrollView() {
44 | lint()
45 | .files(
46 | xml(
47 | "res/layout/activity_home.xml",
48 | """
49 |
52 | """,
53 | ).indented(),
54 | )
55 | .issues(ISSUE_UNSUPPORTED_LAYOUT_ATTRIBUTE)
56 | .run()
57 | .expect(
58 | """
59 | |res/layout/activity_home.xml:3: Error: orientation is not allowed in ScrollView [UnsupportedLayoutAttribute]
60 | | android:orientation="vertical"/>
61 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
62 | |1 errors, 0 warnings
63 | """.trimMargin(),
64 | )
65 | .expectFixDiffs(
66 | """
67 | |Fix for res/layout/activity_home.xml line 2: Remove unnecessary attribute:
68 | |@@ -2 +2
69 | |-
71 | |+
72 | """.trimMargin(),
73 | )
74 | }
75 |
76 | @Test fun orientationInMergeScrollView() {
77 | lint()
78 | .files(
79 | xml(
80 | "res/layout/activity_home.xml",
81 | """
82 |
87 | """,
88 | ).indented(),
89 | )
90 | .issues(ISSUE_UNSUPPORTED_LAYOUT_ATTRIBUTE)
91 | .run()
92 | .expect(
93 | """
94 | |res/layout/activity_home.xml:5: Error: orientation is not allowed in ScrollView [UnsupportedLayoutAttribute]
95 | | android:orientation="vertical"/>
96 | | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
97 | |1 errors, 0 warnings
98 | """.trimMargin(),
99 | )
100 | }
101 |
102 | @Test fun orientationInLinearLayout() {
103 | lint()
104 | .files(
105 | xml(
106 | "res/layout/activity_home.xml",
107 | """
108 |
111 | """,
112 | ).indented(),
113 | )
114 | .issues(ISSUE_UNSUPPORTED_LAYOUT_ATTRIBUTE)
115 | .run()
116 | .expectClean()
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/lint-rules-rxjava2-lint/src/main/kotlin/com/vanniktech/lintrules/rxjava2/RxJava2MethodMissingCheckReturnValueDetector.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage") // We know that Lint APIs aren't final.
2 |
3 | package com.vanniktech.lintrules.rxjava2
4 |
5 | import com.android.tools.lint.client.api.UElementHandler
6 | import com.android.tools.lint.detector.api.Category.Companion.CORRECTNESS
7 | import com.android.tools.lint.detector.api.Detector
8 | import com.android.tools.lint.detector.api.Implementation
9 | import com.android.tools.lint.detector.api.Issue
10 | import com.android.tools.lint.detector.api.JavaContext
11 | import com.android.tools.lint.detector.api.LintFix
12 | import com.android.tools.lint.detector.api.Scope.JAVA_FILE
13 | import com.android.tools.lint.detector.api.Severity.WARNING
14 | import com.intellij.lang.jvm.JvmModifier
15 | import com.intellij.psi.PsiType
16 | import org.jetbrains.kotlin.idea.KotlinLanguage
17 | import org.jetbrains.kotlin.psi.KtProperty
18 | import org.jetbrains.kotlin.util.capitalizeDecapitalize.toUpperCaseAsciiOnly
19 | import org.jetbrains.uast.UAnnotated
20 | import org.jetbrains.uast.UMethod
21 | import java.util.EnumSet
22 |
23 | val ISSUE_METHOD_MISSING_CHECK_RETURN_VALUE = Issue.create(
24 | "RxJava2MethodMissingCheckReturnValue",
25 | "Method is missing the @CheckReturnValue annotation.",
26 | "Methods returning RxJava Reactive Types should be annotated with the @CheckReturnValue annotation. Static analyze tools such as Lint or ErrorProne can detect when the return value of a method is not used. This is usually an indication of a bug. If this is done on purpose (e.g. fire & forget) it should be stated explicitly.",
27 | CORRECTNESS,
28 | PRIORITY,
29 | WARNING,
30 | Implementation(RxJava2MethodMissingCheckReturnValueDetector::class.java, EnumSet.of(JAVA_FILE)),
31 | )
32 |
33 | class RxJava2MethodMissingCheckReturnValueDetector :
34 | Detector(),
35 | Detector.UastScanner {
36 | override fun getApplicableUastTypes() = listOf(UMethod::class.java)
37 |
38 | override fun createUastHandler(context: JavaContext) = CheckReturnValueVisitor(context)
39 |
40 | class CheckReturnValueVisitor(private val context: JavaContext) : UElementHandler() {
41 | override fun visitMethod(node: UMethod) {
42 | val returnType = node.returnType
43 | val isPropertyFunction = node.lang is KotlinLanguage && node.sourcePsi is KtProperty
44 |
45 | if (returnType != null && isTypeThatRequiresAnnotation(returnType) && !isPropertyFunction) {
46 | val hasAnnotatedMethod = context.evaluator.getAllAnnotations(node as UAnnotated, true)
47 | .any { "io.reactivex.annotations.CheckReturnValue" == it.qualifiedName }
48 | if (hasAnnotatedMethod) return
49 |
50 | val hasIgnoredModifier = ignoredModifiers().any { node.javaPsi.hasModifier(it) }
51 | if (hasIgnoredModifier) return
52 |
53 | val modifier = node.javaPsi.modifierList.children.joinToString(separator = " ") { it.text }
54 |
55 | val fix = LintFix.create()
56 | .replace()
57 | .name("Add @CheckReturnValue")
58 | .range(context.getLocation(node))
59 | .shortenNames()
60 | .reformat(true)
61 | .text(modifier)
62 | .with("@io.reactivex.annotations.CheckReturnValue $modifier")
63 | .autoFix()
64 | .build()
65 |
66 | context.report(ISSUE_METHOD_MISSING_CHECK_RETURN_VALUE, node, context.getNameLocation(node), "Method should have `@CheckReturnValue` annotation", fix)
67 | }
68 | }
69 |
70 | private fun isTypeThatRequiresAnnotation(psiType: PsiType): Boolean {
71 | val canonicalText = psiType.canonicalText
72 | .replace("<[\\w.<>]*>".toRegex(), "") // We need to remove the generics.
73 |
74 | return canonicalText.matches("io\\.reactivex\\.\\w+".toRegex()) ||
75 | "io.reactivex.disposables.Disposable" == canonicalText ||
76 | "io.reactivex.disposables.CompositeDisposable" == canonicalText ||
77 | "io.reactivex.observers.TestObserver" == canonicalText ||
78 | "io.reactivex.subscribers.TestSubscriber" == canonicalText
79 | }
80 |
81 | companion object {
82 | internal const val IGNORE_MODIFIERS_PROP = "com.vanniktech.lintrules.rxjava2.RxJava2MethodMissingCheckReturnValueDetector.ignoreMethodAccessModifiers"
83 |
84 | private fun ignoredModifiers(): List = System.getProperty(IGNORE_MODIFIERS_PROP)
85 | ?.split(",")
86 | ?.map { JvmModifier.valueOf(it.toUpperCaseAsciiOnly()) }
87 | .orEmpty()
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------