Kotlin on Code Quality Tools
Motivation
class MyManagerHandlerThatDoesEverything {� fun doX() { }� fun handleThis() { }� fun foo() { }� fun doY() { }� fun isDoingY() { }� fun isDoingX() { }
fun buyBeer() { }� fun drinkBeer() { }� fun fetchPizza() { }� fun doWeHaveEnoughCheese() { }� fun relax() { }� fun bar() { }�}
class BrilliantClass ( val bar: Bar,
val number: Int) {�� class Bar(�� private val value : Int� )�� override fun toString( ) : String = "BrilliantClass(bar=$bar)"�}
class BrilliantClass(� val bar: Bar,� val number: Int�) {� class Bar(� private val value: Int� )�� override fun toString(): String = "BrilliantClass(bar=$bar)"�}
class Foo {� fun foo(map: Array<IntArray>) {� for (i in 0 until map.size) {� for (j in 0 until map[i].size) {� map[i][i] += 1� }� }� }�}
class Foo {� fun foo(map: Array<IntArray>) {� for (i in 0 until map.size) {� for (j in 0 until map[i].size) {� map[i][j] += 1� }� }� }�}
class UpperCasePrinter {� fun print(value: String) {� System.out.println(value.toUpperCase())� }�}
class UpperCasePrinter {� fun print(value: String) {� System.out.println(value.toUpperCase(Locale.US))� }�}
interface Configuration {� fun getFloat(name: String): Float?�}
interface Configuration {� /**� * Returns a [Float] with the given [name] from the configuration� */� fun getFloat(name: String): Float?�}
interface Configuration {� /**� * Returns a [Float] with the given [name] from the configuration.� */� fun getFloat(name: String): Float?�}
dependencies {� implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70"�}
dependencies {� implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.71"�}
class Point(val x: Int, val y: Int)
data class Point(val x: Int, val y: Int)
Code Quality Tools
| Android Lint | Detekt | ktlint |
| Android Lint | Detekt | ktlint |
Java | ✓ | X | X |
| Android Lint | Detekt | ktlint |
Java | ✓ | X | X |
Kotlin | ✓ | ✓ | ✓ |
| Android Lint | Detekt | ktlint |
Java | ✓ | X | X |
Kotlin | ✓ | ✓ | ✓ |
Executable Binary | ✓ | ✓ | ✓ |
| Android Lint | Detekt | ktlint |
Java | ✓ | X | X |
Kotlin | ✓ | ✓ | ✓ |
Executable Binary | ✓ | ✓ | ✓ |
Configuration | ✓ | ✓ | (✓) |
| Android Lint | Detekt | ktlint |
Java | ✓ | X | X |
Kotlin | ✓ | ✓ | ✓ |
Executable Binary | ✓ | ✓ | ✓ |
Configuration | ✓ | ✓ | (✓) |
Static Analysis | ✓ | ✓ | X |
| Android Lint | Detekt | ktlint |
Java | ✓ | X | X |
Kotlin | ✓ | ✓ | ✓ |
Executable Binary | ✓ | ✓ | ✓ |
Configuration | ✓ | ✓ | (✓) |
Static Analysis | ✓ | ✓ | X |
Formatting | (✓) | (✓) | ✓ |
| Android Lint | Detekt | ktlint |
Java | ✓ | X | X |
Kotlin | ✓ | ✓ | ✓ |
Executable Binary | ✓ | ✓ | ✓ |
Configuration | ✓ | ✓ | (✓) |
Static Analysis | ✓ | ✓ | X |
Formatting | (✓) | (✓) | ✓ |
Autocorrection | ✓ | X | ✓ |
| Android Lint | Detekt | ktlint |
Java | ✓ | X | X |
Kotlin | ✓ | ✓ | ✓ |
Executable Binary | ✓ | ✓ | ✓ |
Configuration | ✓ | ✓ | (✓) |
Static Analysis | ✓ | ✓ | X |
Formatting | (✓) | (✓) | ✓ |
Autocorrection | ✓ | X | ✓ |
Reporting | ✓ | ✓ | ✓ |
Android Lint
Android Lint
buildscript {� repositories {� google()� mavenCentral()� }� dependencies {� classpath "com.android.tools.build:gradle:3.3.0-alpha13"� }�}
�apply plugin: "com.android.lint"
Android Lint Task
Android Lint Task
Android Lint HTML report
Android Lint XML report
<?xml version="1.0" encoding="UTF-8"?>�<issues format="4" by="lint 3.3.0-alpha13">�� <issue� id="GradleDependency"� severity="Warning"� message="A newer version of org.jetbrains.kotlin:kotlin-stdlib-jdk8 than 1.2.70 is available: 1.2.71"� category="Correctness"� priority="4"� summary="Obsolete Gradle Dependency"� explanation="This detector looks for usages of libraries where the version you are using is not the current stable release. Using older versions is fine, and there are cases where you deliberately want to stick with an older version. However, you may simply not be aware that a more recent version is available, and that is what this lint check helps find."� errorLine1=" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70""� errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"� quickfix="studio">� <location� file="/Users/nik/dev/kotlin-on-code-quality-tools/build.gradle"� line="28"� column="3"/>� </issue>��</issues>
Android Lint Configuration
<?xml version="1.0" encoding="UTF-8"?>�<lint>� <issue id="UnusedResources">� <ignore regexp="R.string.google_crash_reporting_api_key"/>� </issue>�� <issue id="GradleDependency" severity="ignore"/>�� <issue id="UselessLeaf">� <ignore path="res/layout/main.xml"/>� </issue>�</lint>
Android Lint Configuration
lintOptions {
lintConfig project.file("lint.xml")
enable "TypographyQuotes"� disable "RtlHardcoded", "RtlCompat"� fatal "NewApi"� error "MissingTranslation"� warning "MissingPermission"� ignore "MissingSuperCall"� abortOnError true� warningsAsErrors true� ignoreWarnings false� checkAllWarnings true�}
Extending Android Lint
Detekt
Detekt prerequirement
repositories {� jcenter()�}�
Detekt prerequirement
repositories {� jcenter()�}��configurations {� detekt�}�
Detekt prerequirement
repositories {� jcenter()�}��configurations {� detekt�}��dependencies {� detekt "io.gitlab.arturbosch.detekt:detekt-cli:1.0.0.RC9.2"�}
Detekt Task
def output = new File(project.buildDir, "reports/detekt/")��task detekt(type: JavaExec, group: "verification", description: "Runs detekt.") {� def configFile = file("code_quality_tools/detekt.yml")� inputs.files(project.fileTree(dir: "src", include: "**/*.kt"), configFile)� outputs.dir(output.toString())� main = "io.gitlab.arturbosch.detekt.cli.Main"� classpath = project.configurations.detekt� args = [ "--config", configFile, "--input", project.file("."), "--report",
"plain:$output/plain.txt,xml:$output/checkstyle.xml,html:$output/report.html"
]�}
`
Detekt Configuration
failFast: true��comments:� UndocumentedPublicFunction:� active: false� UndocumentedPublicClass:� active: false
Running Detekt
Running Detekt
Detekt HTML report
Detekt XML report
<?xml version="1.0" encoding="utf-8"?>�<checkstyle version="4.3">�<file name="/Users/nik/dev/GitHub/kotlin-on-code-quality-tools/demo/src/main/kotlin/com/vanniktech/kotlinoncodequalitytools/Configuration.kt">� <error line="4" column="3" severity="warning" message="The first sentence of this KDoc does not end with the correct punctuation." source="detekt.EndOfSentenceFormat" />�</file>�<file name="/Users/nik/dev/GitHub/kotlin-on-code-quality-tools/demo/src/main/kotlin/com/vanniktech/kotlinoncodequalitytools/MyManagerHandlerThatDoesEverything.kt">� <error line="3" column="1" severity="warning" message="Class 'MyManagerHandlerThatDoesEverything' with '12' functions detected. Defined threshold inside classes is set to '11'" source="detekt.TooManyFunctions" />�</file>�<file name="/Users/nik/dev/GitHub/kotlin-on-code-quality-tools/demo/src/main/kotlin/com/vanniktech/kotlinoncodequalitytools/Unused.kt">� <error line="1" column="1" severity="warning" message="The file name 'Unused.kt' does not match the name of the single top-level declaration 'Foo'." source="detekt.MatchingDeclarationName" />� <error line="4" column="3" severity="warning" message="A member is named after the class. This might result in confusion. Either rename the member or change it to a constructor." source="detekt.MemberNameEqualsClassName" />� <error line="6" column="12" severity="warning" message="Private property j is unused." source="detekt.UnusedPrivateMember" />�</file>�<file name="/Users/nik/dev/GitHub/kotlin-on-code-quality-tools/demo/src/main/kotlin/com/vanniktech/kotlinoncodequalitytools/Point.kt">� <error line="3" column="1" severity="warning" message="The class Point defines nofunctionality and only holds data. Consider converting it to a data class." source="detekt.UseDataClass" />�</file>�<file name="/Users/nik/dev/GitHub/kotlin-on-code-quality-tools/demo/src/main/kotlin/com/vanniktech/kotlinoncodequalitytools/BrilliantClass.kt">� <error line="3" column="1" severity="warning" message="The class BrilliantClass defines nofunctionality and only holds data. Consider converting it to a data class." source="detekt.UseDataClass" />� <error line="6" column="7" severity="warning" message="The class Bar defines nofunctionality and only holds data. Consider converting it to a data class." source="detekt.UseDataClass" />� <error line="8" column="6" severity="warning" message="Private property value is unused." source="detekt.UnusedPrivateMember" />�</file>
...�</checkstyle>
Extending Detekt
Extending Detekt
import com.vanniktech.kotlinoncodequalitytools.internal.InternalClass��class InternalImport(val internalClass: InternalClass)
Extending Detekt
apply plugin: "kotlin"�
Extending Detekt
apply plugin: "kotlin"��repositories {� jcenter()�}�
Extending Detekt
apply plugin: "kotlin"��repositories {� jcenter()�}��dependencies {� compileOnly "io.gitlab.arturbosch.detekt:detekt-api:1.0.0.RC9.2"�� testCompile "junit:junit:4.12"� testCompile "org.assertj:assertj-core:3.11.1"� testCompile "io.gitlab.arturbosch.detekt:detekt-api:1.0.0.RC9.2"� testCompile "io.gitlab.arturbosch.detekt:detekt-test:1.0.0.RC9.2"�}
Extending Detekt
class NoInternalImportRule(config: Config = Config.empty) : Rule(config) {�
�
Extending Detekt
class NoInternalImportRule(config: Config = Config.empty) : Rule(config) {� override val issue = Issue(javaClass.simpleName, Severity.Maintainability,� "Don't import from an internal package as they are subject to change.",� Debt.TWENTY_MINS)��
�
Extending Detekt
class NoInternalImportRule(config: Config = Config.empty) : Rule(config) {� override val issue = Issue(javaClass.simpleName, Severity.Maintainability,� "Don't import from an internal package as they are subject to change.",� Debt.TWENTY_MINS)�� override fun visitImportDirective(importDirective: KtImportDirective) {� val import = importDirective.importPath?.pathStr
� if (import?.contains("internal") == true) {� report(CodeSmell(issue, Entity.from(importDirective),� "Importing '$import' which is an internal import."))� }� }�}
Extending Detekt
class CustomRuleSetProvider : RuleSetProvider {��
�
Extending Detekt
class CustomRuleSetProvider : RuleSetProvider {� override val ruleSetId: String = "detekt-custom-rules"�
�
Extending Detekt
class CustomRuleSetProvider : RuleSetProvider {� override val ruleSetId: String = "detekt-custom-rules"�� override fun instance(config: Config)
= RuleSet(ruleSetId, listOf(NoInternalImportRule(config)))�}
Extending Detekt
class CustomRuleSetProvider : RuleSetProvider {� override val ruleSetId: String = "detekt-custom-rules"�� override fun instance(config: Config)
= RuleSet(ruleSetId, listOf(NoInternalImportRule(config)))�}
src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider
com.vanniktech.detektcustomrules.CustomRuleSetProvider
Extending Detekt
class NoInternalImportRuleTest {� @Test fun noInternalImports() {� val findings = NoInternalImportRule().lint("""� import a.b.c� import a.internal.foo� """.trimIndent())
Extending Detekt
class NoInternalImportRuleTest {� @Test fun noInternalImports() {� val findings = NoInternalImportRule().lint("""� import a.b.c� import a.internal.foo� """.trimIndent())
� assertThat(findings).hasSize(1)� assertThat(findings[0].message)
.isEqualTo("Importing 'a.internal.foo' which is an internal import.")� }�}
Extending Detekt
repositories {� jcenter()�}��configurations {� detekt�}��dependencies {� detekt "io.gitlab.arturbosch.detekt:detekt-cli:1.0.0.RC9.2"
detekt project(":custom-detekt-rules")�}
Extending Detekt
ktlint
ktlint prerequirement
repositories {� jcenter()�}
ktlint prerequirement
repositories {� jcenter()�}��configurations {� ktlint�}�
ktlint prerequirement
repositories {� jcenter()�}��configurations {� ktlint�}��dependencies {� ktlint "com.github.shyiko:ktlint:0.29.0"�}
ktlint Task
def outputDir = "${project.buildDir}/reports/ktlint/"��task ktlint(type: JavaExec, group: "verification", description: "Runs ktlint.") {� inputs.files(fileTree(dir: "src", include: "**/*.kt"),
fileTree(dir: ".", include: "**/.editorconfig"))� outputs.dir(outputDir)� main = "com.github.shyiko.ktlint.Main"� classpath = configurations.ktlint� args = [ "--reporter=plain",� "--reporter=checkstyle,output=${outputDir}ktlint-checkstyle-report.xml",� "src/**/*.kt" ]�}
ktlint configuration file
[*.{kt,kts}]�indent_size=2�continuation_indent_size=4�max_line_length=124�insert_final_newline=true
Running ktlint
ktlint XML report
<?xml version="1.0" encoding="utf-8"?>�<checkstyle version="8.0">� <file name="/Users/nik/dev/GitHub/kotlin-on-code-quality-tools/demo/src/main/kotlin/com/vanniktech/kotlinoncodequalitytools/BrilliantClass.kt">� <error line="3" column="22" severity="error" message="Unnecessary space(s)" source="no-multi-spaces" />� <error line="3" column="25" severity="error" message="Parameter should be on a separate line (unless all parameters can fit a single line)" source="parameter-list-wrapping" />� <error line="4" column="2" severity="error" message="Unexpected indentation (expected 2, actual 1)" source="parameter-list-wrapping" />� <error line="4" column="17" severity="error" message="Missing newline before ")"" source="parameter-list-wrapping" />� <error line="3" column="23" severity="error" message="Unexpected spacing around "("" source="paren-spacing" />� <error line="8" column="6" severity="error" message="Unexpected indentation (expected 8, actual 5)" source="parameter-list-wrapping" />� <error line="8" column="24" severity="error" message="Unexpected spacing before ":"" source="colon-spacing" />� <error line="11" column="25" severity="error" message="Unexpected spacing after "("" source="paren-spacing" />� <error line="11" column="28" severity="error" message="Unexpected spacing before ":"" source="colon-spacing" />� <error line="11" column="39" severity="error" message="Unnecessary space(s)" source="no-multi-spaces" />� </file>� <file name="/Users/nik/dev/GitHub/kotlin-on-code-quality-tools/demo/src/main/kotlin/com/vanniktech/kotlinoncodequalitytools/Unused.kt">� <error line="1" column="1" severity="error" message="class Foo should be declared in a file named Foo.kt (cannot be auto-corrected)" source="filename" />� </file>�</checkstyle>
ktlintFormat Task
task ktlintFormat(type: JavaExec, group: "formatting") {� inputs.files(fileTree(dir: "src", include: "**/*.kt"),� fileTree(dir: ".", include: "**/.editorconfig"))� outputs.upToDateWhen { true }� description = "Runs ktlint and autoformats your code."� main = "com.github.shyiko.ktlint.Main"� classpath = configurations.ktlint� args = [ "-F", "src/**/*.kt" ]�}
class BrilliantClass ( val bar: Bar,
val number: Int) {�� class Bar(�� private val value : Int� )�� override fun toString( ) : String = "BrilliantClass(bar=$bar)"�}
Running ktlintFormat
Extending ktlint
Extending ktlint
import com.vanniktech.kotlinoncodequalitytools.internal.InternalClass��class InternalImport(val internalClass: InternalClass)
Extending ktlint
apply plugin: "kotlin"�
Extending ktlint
apply plugin: "kotlin"��repositories {� jcenter()�}�
Extending ktlint
apply plugin: "kotlin"��repositories {� jcenter()�}��dependencies {� compileOnly "com.github.shyiko.ktlint:ktlint-core:0.29.0"�� testCompile "junit:junit:4.12"� testCompile "org.assertj:assertj-core:3.11.1"� testCompile "com.github.shyiko.ktlint:ktlint-core:0.29.0"� testCompile "com.github.shyiko.ktlint:ktlint-test:0.29.0"�}
Extending ktlint
class NoInternalImportRule : Rule("no-internal-import") {�
Extending ktlint
class NoInternalImportRule : Rule("no-internal-import") {� override fun visit(node: ASTNode, autoCorrect: Boolean,� emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) {�
Extending ktlint
class NoInternalImportRule : Rule("no-internal-import") {� override fun visit(node: ASTNode, autoCorrect: Boolean,� emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) {� if (node.elementType == KtStubElementTypes.IMPORT_DIRECTIVE) {� val importDirective = node.psi as KtImportDirective� val path = importDirective.importPath?.pathStr� if (path?.contains("internal") == true) {� emit(node.startOffset, "Importing '$import' which is an internal import", false)� }� }� }�}
Extending ktlint
class CustomRuleSetProvider : RuleSetProvider {�
Extending ktlint
class CustomRuleSetProvider : RuleSetProvider {� override fun get() = RuleSet("custom-ktlint-rules", NoInternalImportRule())�}
Extending ktlint
class CustomRuleSetProvider : RuleSetProvider {� override fun get() = RuleSet("custom-ktlint-rules", NoInternalImportRule())�}
src/main/resources/META-INF/services/com.github.shyiko.ktlint.core.RuleSetProvider
com.vanniktech.ktlintcustomrules.CustomRuleSetProvider
Extending ktlint
class NoInternalImportRuleTest {� @Test fun noWildcardImportsRule() {� assertThat(NoInternalImportRule().lint("""� import a.b.c� import a.internal.foo� """.trimIndent()� )).containsExactly(� LintError(2, 1, "no-internal-import", "Importing 'a.internal.foo' which is an internal import.")� )� }�}
Extending ktlint
repositories {� jcenter()�}��configurations {� ktlint�}��dependencies {� ktlint "com.github.shyiko:ktlint:0.29.0"
ktlint project(":custom-ktlint-rules")�}
Extending ktlint
Resources
Android Lint
Detekt resources
ktlint resources