Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin on Code Quality Tools

Kotlin on Code Quality Tools

Some might have heard about code quality tools such as Checkstyle, PMD, Findbugs, Lint, Detekt and others but which ones are applicable to Kotlin? I'll list a bunch of tools, explain what they can do, touch on how they work and most importantly show you how you can start using them to be more productive and save a lot of time. With the help of these analyzers you can focus on coding conventions in your team, catch a few bugs and concentrate on what really matters when reviewing code.

by Niklas Baudy
presented on June 14, 2018 @moovel

More Decks by Kotlin User Group Hamburg

Other Decks in Programming

Transcript

  1. 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() { } }
  2. class BrilliantClass ( val bar: Bar, val number: Int) {

    class Bar( private val value : Int ) override fun toString( ) : String = "BrilliantClass(bar=$bar)" }
  3. class BrilliantClass( val bar: Bar, val number: Int ) {

    class Bar( private val value: Int ) override fun toString() = "BrilliantClass(bar=$bar)" }
  4. 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 } } } }
  5. 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 } } } }
  6. interface Configuration { /** * Returns a [Float] with the

    given [name] from the configuration */ fun getFloat(name: String): Float? }
  7. interface Configuration { /** * Returns a [Float] with the

    given [name] from the configuration. */ fun getFloat(name: String): Float? }
  8. Android Lint Detekt ktlint Executable Binary ✓ ✓ ✓ Configuration

    ✓ ✓ (✓) Static Analysis ✓ ✓ X Formatting X (✓) ✓
  9. Android Lint Detekt ktlint Executable Binary ✓ ✓ ✓ Configuration

    ✓ ✓ (✓) Static Analysis ✓ ✓ X Formatting X (✓) ✓ Autocorrection ✓ X ✓
  10. Android Lint Detekt ktlint Executable Binary ✓ ✓ ✓ Configuration

    ✓ ✓ (✓) Static Analysis ✓ ✓ X Formatting X (✓) ✓ Autocorrection ✓ X ✓ Reporting ✓ ✓ ✓
  11. Android Lint buildscript { repositories { google() mavenCentral() } dependencies

    { classpath "com.android.tools.build:gradle:3.2.0-alpha18" } } apply plugin: "com.android.lint"
  12. Android Lint XML report <?xml version="1.0" encoding="UTF-8"?> <issues format="4" by="lint

    3.2.0-alpha18"> <issue id="GradleDependency" severity="Warning" message="A newer version of org.jetbrains.kotlin:kotlin-stdlib-jdk8 than 1.2.40 is available: 1.2.41" 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 &quot;org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.40&quot;" errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" quickfix="studio"> <location file="/Users/nik/dev/kotlin-on-code-quality-tools/build.gradle" line="28" column="3"/> </issue> </issues>
  13. 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>
  14. Android Lint Configuration lintOptions { enable "TypographyQuotes" disable "RtlHardcoded", "RtlCompat"

    fatal "NewApi" error "MissingTranslation" warning "MissingPermission" ignore "MissingSuperCall" abortOnError true warningsAsErrors true ignoreWarnings false checkAllWarnings true }
  15. repositories { jcenter() } configurations { detekt } dependencies {

    detekt "io.gitlab.arturbosch.detekt:detekt-cli:1.0.0.RC7-2" } Detekt prerequirement
  16. 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("."), "--output", output ] }
  17. <?xml version="1.0" encoding="utf-8"?> <checkstyle version="4.3"> <file name="/Users/nik/dev/kotlin-on-code-quality-tools/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/kotlin-on-code-quality-tools/src/main/kotlin/com/vanniktech/kotlinoncodequalitytools/MyManagerHandlerThatDoesEverything.kt"> <error line="3" column="1" severity="warning" message="Class &apos;MyManagerHandlerThatDoesEverything&apos; with &apos;12&apos; functions detected. Defined threshold inside classes is set to &apos;11&apos;" source="detekt.TooManyFunctions" /> </file> <file name="/Users/nik/dev/kotlin-on-code-quality-tools/src/main/kotlin/com/vanniktech/kotlinoncodequalitytools/Unused.kt"> <error line="1" column="1" severity="warning" message="The file name &apos;Unused.kt&apos; does not match the name of the single top-level declaration &apos;Foo&apos;." 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/kotlin-on-code-quality-tools/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/kotlin-on-code-quality-tools/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" /> </file> </checkstyle> Detekt XML report
  18. Extending Detekt apply plugin: "kotlin" repositories { jcenter() } dependencies

    { compileOnly "io.gitlab.arturbosch.detekt:detekt-api:1.0.0.RC7-2" testCompile "junit:junit:4.12" testCompile "org.assertj:assertj-core:3.10.0" testCompile "io.gitlab.arturbosch.detekt:detekt-api:1.0.0.RC7-2" testCompile "io.gitlab.arturbosch.detekt:detekt-test:1.0.0.RC7-2" }
  19. Extending Detekt class NoInternalImportRule(config: Config = Config.empty) : Rule(config) {

    override val issue = Issue(javaClass.simpleName, Severity.Style, "Don't import packages 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.")) } } }
  20. Extending Detekt class CustomRuleSetProvider : RuleSetProvider { override val ruleSetId:

    String = "detekt-custom-rules" override fun instance(config: Config) = RuleSet(ruleSetId, listOf(NoInternalImportRule(config))) }
  21. 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
  22. Extending Detekt class NoInternalImportRuleTest { @Test fun noWildcardImportsRule() { 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.") } }
  23. repositories { jcenter() } configurations { detekt } dependencies {

    detekt "io.gitlab.arturbosch.detekt:detekt-cli:1.0.0.RC7-2" detekt project(":custom-detekt-rules") } Extending Detekt
  24. repositories { jcenter() } configurations { ktlint } dependencies {

    ktlint "com.github.shyiko:ktlint:0.23.1" } ktlint prerequirement
  25. 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" ] }
  26. ktlint XML report <?xml version="1.0" encoding="utf-8"?> <checkstyle version="8.0"> <file name="/Users/nik/dev/kotlin-on-code-quality-tools/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 &quot;)&quot;" source="parameter-list-wrapping" /> <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 &quot;:&quot;" source="colon-spacing" /> <error line="11" column="28" severity="error" message="Unexpected spacing before &quot;:&quot;" source="colon-spacing" /> <error line="11" column="39" severity="error" message="Unnecessary space(s)" source="no-multi-spaces" /> </file> </checkstyle>
  27. 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" ] }
  28. Extending ktlint apply plugin: "kotlin" repositories { jcenter() } dependencies

    { compileOnly "com.github.shyiko.ktlint:ktlint-core:0.23.1" testCompile "junit:junit:4.12" testCompile "org.assertj:assertj-core:3.10.0" testCompile "com.github.shyiko.ktlint:ktlint-core:0.23.1" testCompile "com.github.shyiko.ktlint:ktlint-test:0.23.1" }
  29. 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 != null && path.contains("internal")) { emit(node.startOffset, "Importing from an internal package", false) } } } }
  30. Extending ktlint class CustomRuleSetProvider : RuleSetProvider { override fun get()

    = RuleSet("custom-ktlint-rules", NoInternalImportRule()) }
  31. 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
  32. 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 from an internal package") ) } }
  33. repositories { jcenter() } configurations { ktlint } dependencies {

    ktlint "com.github.shyiko:ktlint:0.23.1" ktlint project(":custom-ktlint-rules") } Extending ktlint
  34. repositories { jcenter() } configurations { ktlint } dependencies {

    ktlint "com.github.shyiko:ktlint:0.23.1" ktlint project(":custom-ktlint-rules") } Extending ktlint