├── .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 | --------------------------------------------------------------------------------