Slide 1

Slide 1 text

Lint For Kotlin TakuSemba CyberAgent.Inc

Slide 2

Slide 2 text

@takusemba https://github.com/TakuSemba

Slide 3

Slide 3 text

https://android.github.io/kotlin-guides/style.html

Slide 4

Slide 4 text

ktlint ɾStatic code analysis tool for kotlin ɾBased on official code style ɾBuilt-in formatter ɾCustomizable output

Slide 5

Slide 5 text

ktlint with gradle configurations { ktlint } dependencies { ktlint "com.github.shyiko:ktlint:$KTLINT_VERSION" } check.dependsOn ktlint

Slide 6

Slide 6 text

ktlint with gradle task ktlint(type: JavaExec, group: "kotlin verification") { description = 'Check Kotlin code style.' args 'src/**/*.kt' main = 'com.github.shyiko.ktlint.Main' classpath = configurations.ktlint + sourceSets.main.output } $ ./gradlew tasks

Slide 7

Slide 7 text

ktlint with gradle task ktFormat(type: JavaExec, group: "kotlin Formatting") { description = 'Fix Kotlin code style deviations.' args '-F', 'src/**/*.kt' main = 'com.github.shyiko.ktlint.Main' classpath = configurations.ktlint + sourceSets.main.output } $ ./gradlew tasks

Slide 8

Slide 8 text

ktlint with CLI $ brew install shyiko/ktlint/ktlint $ ktlint "src/**/*.kt"

Slide 9

Slide 9 text

Outputs task ktlint(type: JavaExec, dependsOn: classes) { description = 'Check Kotlin code style.' args 'src/**/*.kt', ‘--reporter=plain' main = 'com.github.shyiko.ktlint.Main' classpath = configurations.ktlint + sourceSets.main.output }

Slide 10

Slide 10 text

Outputs task ktlint(type: JavaExec, dependsOn: classes) { description = 'Check Kotlin code style.' args 'src/**/*.kt', ‘--reporter=plain' main = 'com.github.shyiko.ktlint.Main' classpath = configurations.ktlint + sourceSets.main.output } '--reporter=plain'

Slide 11

Slide 11 text

Outputs --reporter=plain takusemba/ktlint-sample/app/…/Hoge.kt:44:1: Unexpected blank line(s) before "}" takusemba/ktlint-sample/app/…/Fuga.kt:108:1: Needless blank line(s) takusemba/ktlint-sample/app/…/Foo.kt:25:7: Line break before assignment is not allowed takusemba/ktlint-sample/app/…/Bar.kt:47:1: Unexpected blank line(s) before “}" …

Slide 12

Slide 12 text

Outputs --reporter=checkstyle …

Slide 13

Slide 13 text

Outputs --reporter=custom-ktlint-rule class CustomReporter(private val out: PrintStream) : Reporter { private val errors = ArrayList() override fun onLintError( file: String, err: LintError, corrected: Boolean) { errors.add(err) } override fun afterAll() { out.println("hello custom reporter.") out.println("found ${errors.size} errors.") } }

Slide 14

Slide 14 text

Outputs --reporter=custom-ktlint-rule class CustomReporter(private val out: PrintStream) : Reporter { private val errors = ArrayList() override fun onLintError( file: String, err: LintError, corrected: Boolean) { errors.add(err) } override fun afterAll() { out.println("hello custom reporter.") out.println("found ${errors.size} errors.") } } override fun afterAll() { out.println("hello custom reporter.") out.println("found ${errors.size} errors.") }

Slide 15

Slide 15 text

Outputs class CustomReporterProvider : ReporterProvider { override val id: String = “custom-ktlint-reporter" override fun get( out: PrintStream, opt: Map ): Reporter = CustomReporter(out) } --reporter=custom-ktlint-rule

Slide 16

Slide 16 text

Outputs class CustomReporterProvider : ReporterProvider { override val id: String = “custom-ktlint-reporter" override fun get( out: PrintStream, opt: Map ): Reporter = CustomReporter(out) } --reporter=custom-ktlint-rule ): Reporter = CustomReporter(out)

Slide 17

Slide 17 text

Outputs --reporter=custom-ktlint-rule

Slide 18

Slide 18 text

Disable rules import package.* // ktlint-disable no-wildcard-imports import package.* // ktlint-disable disable specific rule disable the whole rule

Slide 19

Slide 19 text

Disable rules

Slide 20

Slide 20 text

Disable rules [*.{kt,kts}] indent_size=2 continuation_indent_size=4 insert_final_newline=unset max_line_length=off .editorconfig

Slide 21

Slide 21 text

Add rules class CustomRule : Rule("custom-rule") { override fun visit( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { if (isLintError) { emit(node.startOffset, “LINT ERROR FOUND!”, false) } } }

Slide 22

Slide 22 text

Add rules class CustomRule : Rule("custom-rule") { override fun visit( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { if (isLintError) { emit(node.startOffset, “LINT ERROR FOUND!”, false) } } } node: ASTNode,

Slide 23

Slide 23 text

Abstract Syntax Tree

Slide 24

Slide 24 text

AST fun add(a: Int, b: Int): Int { return a + b }

Slide 25

Slide 25 text

node node node node node node node node node node AST

Slide 26

Slide 26 text

AST fun add(a: Int, b: Int): Int { return a + b }

Slide 27

Slide 27 text

AST fun add(a: Int, b: Int): Int { return a + b } KtNamedFunction

Slide 28

Slide 28 text

AST { return a + b } KtKeywordToken.fun fun Int : (a: Int, b: Int) add KtToken.IDENTIFIER KtTypeReference KtSingleValueToken.COLON KtParameterList KtBlockExpression

Slide 29

Slide 29 text

AST { return a + b } KtKeywordToken.fun fun Int : (a: Int, b: Int) add KtToken.IDENTIFIER KtTypeReference KtSingleValueToken.COLON KtParameterList KtBlockExpression

Slide 30

Slide 30 text

AST KtSingleValueToken.LBRACE KtReturnExpression KtSingleValueToken.RBRACE } { return a + b

Slide 31

Slide 31 text

AST KtSingleValueToken.LBRACE KtReturnExpression KtSingleValueToken.RBRACE } { return a + b

Slide 32

Slide 32 text

AST KtKeywordToken.return KtToken.IDENTIFIER KtSingleValueToken.PLUS a return + b KtToken.IDENTIFIER

Slide 33

Slide 33 text

node node node node node node node node node node AST

Slide 34

Slide 34 text

Add rules class CustomRule : Rule("custom-rule") { override fun visit( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { if (isLintError) { emit(node.startOffset, “LINT ERROR FOUND!”, false) } } }

Slide 35

Slide 35 text

Add rules class CustomRule : Rule("custom-rule") { override fun visit( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { if (isLintError) { emit(node.startOffset, “LINT ERROR FOUND!”, false) } } } node: ASTNode,

Slide 36

Slide 36 text

Add rules fun add(a: Int, b: Int): Int = a + b // OK fun add(a: Int, b: Int) = a + b // NG

Slide 37

Slide 37 text

fun add(a: Int, b: Int) = a + b // NG

Slide 38

Slide 38 text

Add rules fun add(a: Int, b: Int) = a + b // NG if (node.elementType == KtStubElementTypes.FUNCTION && !node.text.contains('\n')) { val child = node.findChildByType(KtNodeTypes.VALUE_PARAMETER_LIST) ?: return if (child.treeNext.elementType == KtTokens.WHITE_SPACE && child.treeNext.treeNext.elementType == KtTokens.EQ) { emit(node.startOffset, "need return type!!!", false) } }

Slide 39

Slide 39 text

Add rules fun add(a: Int, b: Int) = a + b // NG if (node.elementType == KtStubElementTypes.FUNCTION && !node.text.contains('\n')) { val child = node.findChildByType(KtNodeTypes.VALUE_PARAMETER_LIST) ?: return if (child.treeNext.elementType == KtTokens.WHITE_SPACE && child.treeNext.treeNext.elementType == KtTokens.EQ) { emit(node.startOffset, "need return type!!!", false) } } if (node.elementType == KtStubElementTypes.FUNCTION && !node.text.contains('\n')) { }

Slide 40

Slide 40 text

Add rules fun add(a: Int, b: Int) = a + b // NG (a: Int, b: Int) if (node.elementType == KtStubElementTypes.FUNCTION && !node.text.contains('\n')) { val child = node.findChildByType(KtNodeTypes.VALUE_PARAMETER_LIST) ?: return if (child.treeNext.elementType == KtTokens.WHITE_SPACE && child.treeNext.treeNext.elementType == KtTokens.EQ) { emit(node.startOffset, "need return type!!!”, false) } } val child = node.findChildByType(KtNodeTypes.VALUE_PARAMETER_LIST) ?: return

Slide 41

Slide 41 text

Add rules fun add(a: Int, b: Int) = a + b // NG if (node.elementType == KtStubElementTypes.FUNCTION && !node.text.contains('\n')) { val child = node.findChildByType(KtNodeTypes.VALUE_PARAMETER_LIST) ?: return if (child.treeNext.elementType == KtTokens.WHITE_SPACE && child.treeNext.treeNext.elementType == KtTokens.EQ) { emit(node.startOffset, "need return type!!!", false) } } if (child.treeNext.elementType == KtTokens.WHITE_SPACE && child.treeNext.treeNext.elementType == KtTokens.EQ) { =

Slide 42

Slide 42 text

Add rules fun add(a: Int, b: Int) = a + b // NG if (node.elementType == KtStubElementTypes.FUNCTION && !node.text.contains('\n')) { val child = node.findChildByType(KtNodeTypes.VALUE_PARAMETER_LIST) ?: return if (child.treeNext.elementType == KtTokens.WHITE_SPACE && child.treeNext.treeNext.elementType == KtTokens.EQ) { emit(node.startOffset, "need return type!!!", false) } } fun add(a: Int, b: Int) = a + b emit(node.startOffset, "need return type!!!", false)

Slide 43

Slide 43 text

Add rules class ExpressionFunctionRule : Rule("one-line-function") { override fun visit( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { if (node.elementType == KtStubElementTypes.FUNCTION && !node.text.contains('\n')) { val child = node.findChildByType(KtNodeTypes.VALUE_PARAMETER_LIST) ?: return if (child.treeNext.elementType == KtTokens.WHITE_SPACE && child.treeNext.treeNext.elementType == KtTokens.EQ) { emit(node.startOffset, "need return type!!!", false) } } } }

Slide 44

Slide 44 text

Add rules class CustomRuleSetProvider : RuleSetProvider { override fun get(): RuleSet = RuleSet( “custom-rule", ExpressionFunctionRule() ) }

Slide 45

Slide 45 text

Add rules class CustomRuleSetProvider : RuleSetProvider { override fun get(): RuleSet = RuleSet( “custom-rule", ExpressionFunctionRule() ) } ExpressionFunctionRule()

Slide 46

Slide 46 text

Add rules

Slide 47

Slide 47 text

Add rules $ ktlint "**/Hoge.kt" -R ktrule/build/libs/ktrule.jar --relative $ ./gradlew jar

Slide 48

Slide 48 text

Add rules val rule = ExpressionFunctionRule() assertThat(rule.lint( """ fun add(a: Int, b: Int): Int = a + b """ .trimIndent()) ).isEqualTo(emptyList()) assertThat(rule.lint( """ fun add(a: Int, b: Int) = a + b """ .trimIndent()) ).isEqualTo(listOf(LintError(1, 1, "one-line-function", "need return type!!!")))

Slide 49

Slide 49 text

Add rules val rule = ExpressionFunctionRule() assertThat(rule.lint( """ fun add(a: Int, b: Int): Int = a + b """ .trimIndent()) ).isEqualTo(emptyList()) assertThat(rule.lint( """ fun add(a: Int, b: Int) = a + b """ .trimIndent()) ).isEqualTo(listOf(LintError(1, 1, "one-line-function", "need return type!!!"))) val rule = ExpressionFunctionRule() assertThat(rule.lint( """ fun add(a: Int, b: Int): Int = a + b """ .trimIndent()) ).isEqualTo(emptyList())

Slide 50

Slide 50 text

Add rules val rule = ExpressionFunctionRule() assertThat(rule.lint( """ fun add(a: Int, b: Int): Int = a + b """ .trimIndent()) ).isEqualTo(emptyList()) assertThat(rule.lint( """ fun add(a: Int, b: Int) = a + b """ .trimIndent()) ).isEqualTo(listOf(LintError(1, 1, "one-line-function", "need return type!!!"))) assertThat(rule.lint( """ fun add(a: Int, b: Int) = a + b """ .trimIndent()) ).isEqualTo(listOf(LintError(1, 1, "one-line-function", "need return type!!!")))

Slide 51

Slide 51 text

Add rules

Slide 52

Slide 52 text

with Danger

Slide 53

Slide 53 text

with Danger BQQTSDNBJOKBWBDPNUBLVTFNCB)PHFLU Danger # Dangerfile checkstyle_format.base_path = Dir.pwd checkstyle_format.report ‘path/to/result.xml’

Slide 54

Slide 54 text

Lint For Kotlin https://github.com/takusemba https://twitter.com/takusemba