1 of 94

Kotlin on Code Quality Tools

2 of 94

Motivation

3 of 94

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() { }}

4 of 94

class BrilliantClass ( val bar: Bar,

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

5 of 94

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

6 of 94

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}}}}

7 of 94

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}}}}

8 of 94

class UpperCasePrinter {fun print(value: String) {System.out.println(value.toUpperCase())}}

9 of 94

class UpperCasePrinter { fun print(value: String) {System.out.println(value.toUpperCase(Locale.US))}}

10 of 94

interface Configuration {fun getFloat(name: String): Float?}

11 of 94

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

12 of 94

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

13 of 94

dependencies {� implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70"�}

14 of 94

dependencies {� implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.71"�}

15 of 94

class Point(val x: Int, val y: Int)

16 of 94

data class Point(val x: Int, val y: Int)

17 of 94

Code Quality Tools

18 of 94

19 of 94

20 of 94

21 of 94

Android Lint

Detekt

ktlint

22 of 94

Android Lint

Detekt

ktlint

Java

X

X

23 of 94

Android Lint

Detekt

ktlint

Java

X

X

Kotlin

24 of 94

Android Lint

Detekt

ktlint

Java

X

X

Kotlin

Executable Binary

25 of 94

Android Lint

Detekt

ktlint

Java

X

X

Kotlin

Executable Binary

Configuration

(✓)

26 of 94

Android Lint

Detekt

ktlint

Java

X

X

Kotlin

Executable Binary

Configuration

(✓)

Static Analysis

X

27 of 94

Android Lint

Detekt

ktlint

Java

X

X

Kotlin

Executable Binary

Configuration

(✓)

Static Analysis

X

Formatting

(✓)

(✓)

28 of 94

Android Lint

Detekt

ktlint

Java

X

X

Kotlin

Executable Binary

Configuration

(✓)

Static Analysis

X

Formatting

(✓)

(✓)

Autocorrection

X

29 of 94

Android Lint

Detekt

ktlint

Java

X

X

Kotlin

Executable Binary

Configuration

(✓)

Static Analysis

X

Formatting

(✓)

(✓)

Autocorrection

X

Reporting

30 of 94

Android Lint

31 of 94

Android Lint

buildscript {� repositories {� google()� mavenCentral()}� dependencies {� classpath "com.android.tools.build:gradle:3.3.0-alpha13"}}

�apply plugin: "com.android.lint"

32 of 94

Android Lint Task

33 of 94

Android Lint Task

34 of 94

Android Lint HTML report

35 of 94

Android Lint XML report

<?xml version="1.0" encoding="UTF-8"?><issues format="4" by="lint 3.3.0-alpha13">�� <issueid="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 &quot;org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70&quot;"errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"quickfix="studio"><locationfile="/Users/nik/dev/kotlin-on-code-quality-tools/build.gradle"line="28"column="3"/></issue>��</issues>

36 of 94

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>

37 of 94

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}

38 of 94

Extending Android Lint

39 of 94

Detekt

40 of 94

Detekt prerequirement

repositories {� jcenter()}

41 of 94

Detekt prerequirement

repositories {� jcenter()}��configurations {� detekt�}

42 of 94

Detekt prerequirement

repositories {� jcenter()}��configurations {� detekt�}��dependencies {� detekt "io.gitlab.arturbosch.detekt:detekt-cli:1.0.0.RC9.2"}

43 of 94

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"

]}

`

44 of 94

Detekt Configuration

failFast: true��comments:� UndocumentedPublicFunction:� active: false� UndocumentedPublicClass:� active: false

45 of 94

Running Detekt

46 of 94

Running Detekt

47 of 94

Detekt HTML report

48 of 94

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 &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/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 &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/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>

49 of 94

Extending Detekt

50 of 94

Extending Detekt

import com.vanniktech.kotlinoncodequalitytools.internal.InternalClass��class InternalImport(val internalClass: InternalClass)

51 of 94

Extending Detekt

apply plugin: "kotlin"

52 of 94

Extending Detekt

apply plugin: "kotlin"��repositories {� jcenter()}

53 of 94

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"}

54 of 94

Extending Detekt

class NoInternalImportRule(config: Config = Config.empty) : Rule(config) {

55 of 94

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)��

56 of 94

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."))}}}

57 of 94

Extending Detekt

class CustomRuleSetProvider : RuleSetProvider {��

58 of 94

Extending Detekt

class CustomRuleSetProvider : RuleSetProvider {override val ruleSetId: String = "detekt-custom-rules"

59 of 94

Extending Detekt

class CustomRuleSetProvider : RuleSetProvider {override val ruleSetId: String = "detekt-custom-rules"�� override fun instance(config: Config)

= RuleSet(ruleSetId, listOf(NoInternalImportRule(config)))}

60 of 94

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

61 of 94

Extending Detekt

class NoInternalImportRuleTest {@Test fun noInternalImports() {val findings = NoInternalImportRule().lint("""� import a.b.c� import a.internal.foo� """.trimIndent())

62 of 94

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.")}}

63 of 94

Extending Detekt

repositories {� jcenter()}��configurations {� detekt�}��dependencies {� detekt "io.gitlab.arturbosch.detekt:detekt-cli:1.0.0.RC9.2"

detekt project(":custom-detekt-rules")�}

64 of 94

Extending Detekt

65 of 94

ktlint

66 of 94

ktlint prerequirement

repositories {� jcenter()}

67 of 94

ktlint prerequirement

repositories {� jcenter()}��configurations {� ktlint�}

68 of 94

ktlint prerequirement

repositories {� jcenter()}��configurations {� ktlint�}��dependencies {� ktlint "com.github.shyiko:ktlint:0.29.0"}

69 of 94

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" ]}

70 of 94

ktlint configuration file

[*.{kt,kts}]�indent_size=2�continuation_indent_size=4�max_line_length=124�insert_final_newline=true

71 of 94

Running ktlint

72 of 94

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 &quot;)&quot;" source="parameter-list-wrapping" /><error line="3" column="23" severity="error" message="Unexpected spacing around &quot;(&quot;" 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 &quot;:&quot;" source="colon-spacing" /><error line="11" column="25" severity="error" message="Unexpected spacing after &quot;(&quot;" source="paren-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><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>

73 of 94

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" ]}

74 of 94

class BrilliantClass ( val bar: Bar,

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

75 of 94

Running ktlintFormat

76 of 94

Extending ktlint

77 of 94

Extending ktlint

import com.vanniktech.kotlinoncodequalitytools.internal.InternalClass��class InternalImport(val internalClass: InternalClass)

78 of 94

Extending ktlint

apply plugin: "kotlin"

79 of 94

Extending ktlint

apply plugin: "kotlin"��repositories {� jcenter()}

80 of 94

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"}

81 of 94

Extending ktlint

class NoInternalImportRule : Rule("no-internal-import") {

82 of 94

Extending ktlint

class NoInternalImportRule : Rule("no-internal-import") {override fun visit(node: ASTNode, autoCorrect: Boolean,� emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) {

83 of 94

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 KtImportDirectiveval path = importDirective.importPath?.pathStr� if (path?.contains("internal") == true) {� emit(node.startOffset, "Importing '$import' which is an internal import", false)}}}}

84 of 94

Extending ktlint

class CustomRuleSetProvider : RuleSetProvider {

85 of 94

Extending ktlint

class CustomRuleSetProvider : RuleSetProvider {override fun get() = RuleSet("custom-ktlint-rules", NoInternalImportRule())}

86 of 94

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

87 of 94

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."))}}

88 of 94

Extending ktlint

repositories {� jcenter()}��configurations {� ktlint�}��dependencies {� ktlint "com.github.shyiko:ktlint:0.29.0"

ktlint project(":custom-ktlint-rules")�}

89 of 94

Extending ktlint

90 of 94

Resources

91 of 94

Android Lint

92 of 94

Detekt resources

93 of 94

ktlint resources

94 of 94

Thank you