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

Lint that Kotlin

Lint that Kotlin

Code formatting is often not taken seriously and sometimes even omitted, but it's actually something that brings enjoyment to our eyes while doing code reviews. In this talk, we will dig into ktlint (Kotlin formatting linter), its internals (UAST, Psi), how to create a custom rule and how to have the formatting automated.

Roman Zavarnitsyn

May 08, 2019
Tweet

More Decks by Roman Zavarnitsyn

Other Decks in Programming

Transcript

  1. Kotlin code formatting fun someMethod(): Unit { val someVar =

    resolveVar() if (someVar>=0) { // valid println("Var is valid and is equal to ${someVar}") } }
  2. Kotlin code formatting fun someMethod(): Unit { val someVar =

    resolveVar() if (someVar>=0) { // valid println("Var is valid and is equal to ${someVar}") } }
  3. Kotlin code formatting fun someMethod(): Unit { val someVar =

    resolveVar() if (someVar>=0) { // valid println("Var is valid and is equal to ${someVar}") } }
  4. Kotlin code formatting fun someMethod(): Unit { val someVar =

    resolveVar() if (someVar>=0) { // valid println("Var is valid and is equal to ${someVar}") } }
  5. Kotlin code formatting fun someMethod(): Unit { val someVar =

    resolveVar() if (someVar>=0) { // valid println("Var is valid and is equal to ${someVar}") } }
  6. Kotlin code formatting fun someMethod(): Unit { val someVar =

    resolveVar() if (someVar>=0) { // valid println("Var is valid and is equal to ${someVar}") } // empty line }
  7. Kotlin code formatting fun someMethod(): Unit { val someVar =

    resolveVar() if (someVar>=0) { // valid println("Var is valid and is equal to ${someVar}") } }
  8. ktlint • Reflects official Kotlin style guide (+ Android) •

    Built-in formatter • Customizable output (plain, json, checkstyle, html) • Customizable ruleset • Integration with Maven, Gradle, IntelliJ IDEA
  9. $ sh ktlint SomeFile.kt fun someMethod(): Unit { val someVar

    = resolveVar() if (someVar>=0) { // valid println("Var is valid and is equal to ${someVar}") } } SomeFile.kt:1:19: Unnecessary "Unit" return type SomeFile.kt:2:1: Unexpected newline before "{" SomeFile.kt:4:16: Missing spacing around ">=" SomeFile.kt:5:20: Redundant curly braces SomeFile.kt:7:1: Unexpected blank line(s) before "}"
  10. $ sh ktlint SomeFile.kt --format fun someMethod(): Unit { {

    val someVar = resolveVar() if (someVar>=0) { // valid println("Var is valid and is equal to ${someVar}") } } } }
  11. $ sh ktlint SomeFile.kt --format fun someMethod() { { val

    someVar = resolveVar() if (someVar >= 0) { // valid println("Var is valid and is equal to $someVar") } } } }
  12. import resolver.resolveVar fun someMethod() { { val someVar = resolveVar()

    if (someVar >= 0) { // valid println("Var is valid and is equal to $someVar") } } } } File
  13. import resolver.resolveVar fun someMethod() { val someVar = resolveVar() if

    (someVar >= 0) { // valid println("Var is valid and is equal to $someVar") } } File • Imports
  14. import resolver.resolveVar fun someMethod() { { val someVar = resolveVar()

    if (someVar >= 0) { // valid println("Var is valid and is equal to $someVar") } } } } File • Imports • Functions
  15. import resolver.resolveVar fun someMethod() { val someVar = resolveVar() if

    (someVar >= 0) { // valid println("Var is valid and is equal to $someVar") } } File • Imports • Functions • Properties
  16. import resolver.resolveVar fun someMethod() { val someVar = resolveVar() if

    (someVar >= 0) { // valid println("Var is valid and is equal to $someVar") } } } } File • Imports • Functions • Properties • Expressions
  17. import resolver.resolveVar fun someMethod() { val someVar = resolveVar() if

    (someVar >= 0) { // valid println("Var is valid and is equal to $someVar") } } } File • Imports • Functions • Properties • Expressions • Operations
  18. import resolver.resolveVar fun someMethod() { val someVar = resolveVar() if

    (someVar >= 0) { // valid println("Var is valid and is equal to $someVar") }} }} File • Imports • Functions • Properties • Expressions • Operations Psi Psi Psi Psi Psi Psi
  19. import resolver.resolveVar fun someMethod() {{ val someVar = resolveVar() if

    (someVar >= 0) {{// valid println("Var is valid and is equal to $someVar") }} }} PsiFile PsiImports PsiFunctions PsiProperties PsiExpressions PsiOperations
  20. import resolver.resolveVar fun someMethod() {{ val someVar = resolveVar() if

    (someVar >= 0) {{// valid println("Var is valid and is equal to $someVar") }} }} PsiFile : PsiElement PsiImports : PsiElement PsiFunctions : PsiElement PsiProperties : PsiElement PsiExpressions : PsiElement PsiOperations : PsiElement
  21. import resolver.resolveVar fun someMethod() {{ val someVar = resolveVar() if

    (someVar >= 0) {{// valid println("Var is valid and is equal to $someVar") }} }} PsiFile : PsiElement PsiImports : PsiElement PsiFunctions : PsiElement PsiProperties : PsiElement PsiExpressions : PsiElement PsiOperations : PsiElement PsiWhiteSpace : PsiElement
  22. $ sh ktlint SomeFile.kt --print-ast ~.psi.KtFile (FILE) ~.psi.KtPackageDirective (PACKAGE_DIRECTIVE) ""

    ~.psi.KtImportList (IMPORT_LIST) "" ~.psi.KtNamedFunction (FUN) ~.c.i.p.impl.source.tree.LeafPsiElement (FUN_KEYWORD) "fun" ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " " ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "someMethod" ~.psi.KtParameterList (VALUE_PARAMETER_LIST) ~.c.i.p.impl.source.tree.LeafPsiElement (LPAR) "(" ~.c.i.p.impl.source.tree.LeafPsiElement (RPAR) ")" ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " " ~.psi.KtBlockExpression (BLOCK) ~.c.i.p.impl.source.tree.LeafPsiElement (LBRACE) "{" ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n " ~.psi.KtProperty (PROPERTY) ~.c.i.p.impl.source.tree.LeafPsiElement (VAL_KEYWORD) "val" ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " " ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "someVar" fun someMethod() { val someVar = resolveVar() if (someVar >= 0) { // valid println("Var is valid and is equal to $someVar") } }
  23. $ sh ktlint SomeFile.kt --print-ast ~.psi.KtFile (FILE) ~.psi.KtPackageDirective (PACKAGE_DIRECTIVE) ""

    ~.psi.KtImportList (IMPORT_LIST) "" ~.psi.KtNamedFunction (FUN) ~.c.i.p.impl.source.tree.LeafPsiElement (FUN_KEYWORD) "fun" ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " " ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "someMethod" ~.psi.KtParameterList (VALUE_PARAMETER_LIST) ~.c.i.p.impl.source.tree.LeafPsiElement (LPAR) "(" ~.c.i.p.impl.source.tree.LeafPsiElement (RPAR) ")" ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " " ~.psi.KtBlockExpression (BLOCK) ~.c.i.p.impl.source.tree.LeafPsiElement (LBRACE) "{" ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n " ~.psi.KtProperty (PROPERTY) ~.c.i.p.impl.source.tree.LeafPsiElement (VAL_KEYWORD) "val" ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " " ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "someVar" fun someMethod() { val someVar = resolveVar() if (someVar >= 0) { // valid println("Var is valid and is equal to $someVar") } }
  24. $ sh ktlint SomeFile.kt --print-ast ~.psi.KtFile (FILE) ~.psi.KtPackageDirective (PACKAGE_DIRECTIVE) ""

    ~.psi.KtImportList (IMPORT_LIST) "" ~.psi.KtNamedFunction (FUN) ~.c.i.p.impl.source.tree.LeafPsiElement (FUN_KEYWORD) "fun" ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " " ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "someMethod" ~.psi.KtParameterList (VALUE_PARAMETER_LIST) ~.c.i.p.impl.source.tree.LeafPsiElement (LPAR) "(" ~.c.i.p.impl.source.tree.LeafPsiElement (RPAR) ")" ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " " ~.psi.KtBlockExpression (BLOCK) ~.c.i.p.impl.source.tree.LeafPsiElement (LBRACE) "{" ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) "\n " ~.psi.KtProperty (PROPERTY) ~.c.i.p.impl.source.tree.LeafPsiElement (VAL_KEYWORD) "val" ~.c.i.p.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) " " ~.c.i.p.impl.source.tree.LeafPsiElement (IDENTIFIER) "someVar" fun someMethod() { val someVar = resolveVar() if (someVar >= 0) { // valid println("Var is valid and is equal to $someVar") } }
  25. import resolver.resolveVar fun someMethod() { val someVar = resolveVar() if

    (someVar >= 0) { // valid println("Var is valid and is equal to $someVar") } } val file: KtFile
  26. import resolver.resolveVar fun someMethod() { val someVar = resolveVar() if

    (someVar >= 0) { // valid println("Var is valid and is equal to $someVar") } } val file: KtFile val method = file.findChildByTypeOrClass(KtStubElementTypes.FUNCTION_TYPE, KtFunctionType::class.java)
  27. import resolver.resolveVar fun someMethod() { val someVar = resolveVar() if

    (someVar >= 0) { // valid println("Var is valid and is equal to $someVar") } } val file: KtFile val method = file.findChildByTypeOrClass(KtStubElementTypes.FUNCTION_TYPE, KtFunctionType::class.java) val operator = method.findDescendantOfType<LeafPsiElement> { it.elementType == KtTokens.GTEQ }
  28. Range operator for (i in 1..100) when (score) { in

    1..3 -> "bad" in 4..8 -> "good" in 9..10 -> "excellent" } }
  29. Range operator when (score) { in 1 .. 3 ->

    "bad" in 4 .. 8 -> "good" in 9 .. 10 -> "excellent" } } for (i in 1 .. 100)
  30. Writing a custom rule class SpacingAroundRangeOperatorRule : Rule("range-spacing") { override

    fun visit( node: ASTNode, autoCorrect: Boolean, emit: ( offset: Int, errorMessage: String, canBeAutoCorrected: Boolean ) -> Unit ) { // Implementation... } } for (i in 1 .. 100)
  31. Writing a custom rule class SpacingAroundRangeOperatorRule : Rule("range-spacing") { override

    fun visit( node: ASTNode, autoCorrect: Boolean, emit: ( offset: Int, errorMessage: String, canBeAutoCorrected: Boolean ) -> Unit ) { // Implementation... } } for (i in 1 .. 100)
  32. Writing a custom rule class SpacingAroundRangeOperatorRule : Rule("range-spacing") { override

    fun visit( node: ASTNode, autoCorrect: Boolean, emit: ( offset: Int, errorMessage: String, canBeAutoCorrected: Boolean ) -> Unit ) { // Implementation... } } for (i in 1 .. 100)
  33. Writing a custom rule class SpacingAroundRangeOperatorRule : Rule("range-spacing") { override

    fun visit( node: ASTNode, autoCorrect: Boolean, emit: ( offset: Int, errorMessage: String, canBeAutoCorrected: Boolean ) -> Unit ) { // Implementation... } } } } for (i in 1 .. 100)
  34. Writing a custom rule class SpacingAroundRangeOperatorRule : Rule("range-spacing") { override

    fun visit(...) { if (node.elementType == RANGE) { val prevLeaf = node.prevLeaf() val nextLeaf = node.nextLeaf() } } } } } for (i in 1 .. 100)
  35. Writing a custom rule class SpacingAroundRangeOperatorRule : Rule("range-spacing") { override

    fun visit(...) { if (node.elementType == RANGE) { val prevLeaf = node.prevLeaf() val nextLeaf = node.nextLeaf() } } } KtSingleValueToken RANGE = new KtSingleValueToken("RANGE", ".."); for (i in 1 .. 100)
  36. Writing a custom rule class SpacingAroundRangeOperatorRule : Rule("range-spacing") { override

    fun visit(...) { if (node.elementType == RANGE) { val prevLeaf = node.prevLeaf() val nextLeaf = node.nextLeaf() } } } for (i in 1 .. 100)
  37. Writing a custom rule class SpacingAroundRangeOperatorRule : Rule("range-spacing") {{ override

    fun visit(...) {{ if (node.elementType == RANGE) {{ val prevLeaf = node.prevLeaf() val nextLeaf = node.nextLeaf() }} }} }} for (i in 1 .. 100)
  38. Writing a custom rule class SpacingAroundRangeOperatorRule : Rule("range-spacing") {{ override

    fun visit(...) {{ if (node.elementType == RANGE) {{ val prevLeaf = node.prevLeaf() val nextLeaf = node.nextLeaf() when {{ prevLeaf is PsiWhiteSpace && nextLeaf is PsiWhiteSpace -> {{ emit(node.startOffset, "Unexpected spacing around \"..\"") if (autoCorrect) { prevLeaf.treeParent.removeChild(prevLeaf) nextLeaf.treeParent.removeChild(nextLeaf) }} }} }} }} }}} }} for (i in 1 .. 100)
  39. Writing a custom rule class SpacingAroundRangeOperatorRule : Rule("range-spacing") { override

    fun visit(...) { if (node.elementType == RANGE) { val prevLeaf = node.prevLeaf() val nextLeaf = node.nextLeaf() when { prevLeaf is PsiWhiteSpace && nextLeaf is PsiWhiteSpace -> { emit(node.startOffset, "Unexpected spacing around \"..\"") if (autoCorrect) { prevLeaf.treeParent.removeChild(prevLeaf) nextLeaf.treeParent.removeChild(nextLeaf) } } } } } } for (i in 1 .. 100)
  40. Writing a custom rule class SpacingAroundRangeOperatorRule : Rule("range-spacing") { override

    fun visit(...) { if (node.elementType == RANGE) { val prevLeaf = node.prevLeaf() val nextLeaf = node.nextLeaf() when { prevLeaf is PsiWhiteSpace && nextLeaf is PsiWhiteSpace -> { emit(node.startOffset, "Unexpected spacing around \"..\"") if (autoCorrect) { prevLeaf.treeParent.removeChild(prevLeaf) nextLeaf.treeParent.removeChild(nextLeaf) } } } } } } } for (i in 1 .. 100)
  41. Writing a custom rule class SpacingAroundRangeOperatorRule : Rule("range-spacing") {{ override

    fun visit(...) {{ if (node.elementType == RANGE) {{ val prevLeaf = node.prevLeaf() val nextLeaf = node.nextLeaf() when {{ prevLeaf is PsiWhiteSpace && nextLeaf is PsiWhiteSpace -> {{{ emit(node.startOffset, "Unexpected spacing around \"..\"") if (autoCorrect) {{ prevLeaf.treeParent.removeChild(prevLeaf) nextLeaf.treeParent.removeChild(nextLeaf) }} }} }} }} }} }} for (i in 1 .. 100)
  42. Writing a custom rule class SpacingAroundRangeOperatorRule : Rule("range-spacing") {{ override

    fun visit(...) {{ if (node.elementType == RANGE) {{ val prevLeaf = node.prevLeaf() val nextLeaf = node.nextLeaf() when {{ ... prevLeaf is PsiWhiteSpace -> {{ emit(node.startOffset, "Unexpected spacing before \"..\"") if (autoCorrect) {{ prevLeaf.treeParent.removeChild(prevLeaf) }} }} }} }} }} }} for (i in 1 .. 100)
  43. Writing a custom rule class SpacingAroundRangeOperatorRule : Rule("range-spacing") {{ override

    fun visit(...) {{ if (node.elementType == RANGE) {{ val prevLeaf = node.prevLeaf() val nextLeaf = node.nextLeaf() when {{ ... nextLeaf is PsiWhiteSpace -> {{ emit(node.startOffset, "Unexpected spacing after \"..\"") if (autoCorrect) {{ nextLeaf.treeParent.removeChild(nextLeaf) }} }} }} }} }} }} for (i in 1 .. 100)
  44. Automation • Prevent everyone from pushing unformatted code • Moreover,

    do the formatting automatically on each commit • If the unformatted code leaks to remote, CI bot leaves a comment on a pull request
  45. Git hooks (pre-commit) #!/bin/bash echo "Running ktlint formatter..." files=$(git diff

    --cached --name-only --diff-filter=ACM | grep '\.kt\?$') # Format only staged files using ktlint chmod a+x tools/ktlint/ktlint tools/ktlint/ktlint -F --android $files # Stage the changes after formatting git add $files echo "ktlint formatted the code successfully." exit 0
  46. files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.kt\?$') --- a/sample/src/main/kotlin/com/sample/DeletedClass.java

    +++ b/sample/src/main/kotlin/com/sample/SomeClass.java @@ -0,0 +1,4 @@ +class SomeClass { +} +++ b/sample/src/main/kotlin/com/sample/SomeFile.kt @@ -0,0 +1,8 @@ +fun someMethod(): Unit +{ + val someVar = resolveVar() + if (someVar>=0) { // valid + println("Var is valid and is equal to ${someVar}") + } + +}
  47. Git hooks (pre-commit) #!/bin/bash echo "Running ktlint formatter..." files=$(git diff

    --cached --name-only --diff-filter=ACM | grep '\.kt\?$') # Format only staged files using ktlint chmod a+x tools/ktlint/ktlint tools/ktlint/ktlint -F --android $files # Stage the changes after formatting git add $files echo "ktlint formatted the code successfully." exit 0
  48. Git hooks (pre-commit) #!/bin/bash echo "Running ktlint formatter..." files=$(git diff

    --cached --name-only --diff-filter=ACM | grep '\.kt\?$') # Format only staged files using ktlint chmod a+x tools/ktlint/ktlint tools/ktlint/ktlint -F --android $files # Stage the changes after formatting git add $files echo "ktlint formatted the code successfully." exit 0
  49. Git hooks (pre-commit) #!/bin/bash echo "Running ktlint formatter..." files=$(git diff

    --cached --name-only --diff-filter=ACM | grep '\.kt\?$') # Format only staged files using ktlint chmod a+x tools/ktlint/ktlint tools/ktlint/ktlint -F --android $files # Stage the changes after formatting git add $files echo "ktlint formatted the code successfully." exit 0
  50. CI setup (Jenkins + Bitbucket) • A job, which runs

    over the opened pull requests • When there’s a new commit on PR - runs static code analysis (ktlint is part of them) • A Jenkins plugin that allows to leave comments on PRs