From 3866e09dd36f5559f3a7c9f8c1d080f3211d095a Mon Sep 17 00:00:00 2001 From: paul-dingemans Date: Mon, 20 Dec 2021 21:19:47 +0100 Subject: [PATCH] Fix indentation of delegated super type call entry (#1305) * Fix indentation of delegated super type call entry Forces the BY keyword in a super type call entry to be indented when preceded by a new line Closes #1210 * Fix negative indent level after formatting a supertype delegate Co-authored-by: Paul Dingemans --- CHANGELOG.md | 1 + .../ruleset/standard/IndentationRule.kt | 28 +++-- .../ruleset/standard/IndentationRuleTest.kt | 100 ++++++++++++++++-- 3 files changed, 113 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdf9f24e21..bde4a4dde2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). - Do not remove import which is used as markdown link in KDoc only (`no-unused-imports`) ([#1282](https://github.com/pinterest/ktlint/issues/1282)) - Fix indentation of secondary constructor (`indent`) ([#1222](https://github.com/pinterest/ktlint/issues/1222)) - Fix alignment of arrow when trailing comma is missing in when entry (`trailing-comma`) ([#1312](https://github.com/pinterest/ktlint/issues/1312)) +- Fix indent of delegated super type entry (`indent`) ([#1210](https://github.com/pinterest/ktlint/issues/1210)) ### Changed - Update Kotlin version to `1.6.0` release diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt index 2cfd74b72d..fc7d3d6374 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt @@ -518,12 +518,16 @@ class IndentationRule : Rule("indent"), Rule.Modifier.RestrictToRootLast { val pairedLeft = n.pairedLeft() if (prevBlockLine != blockLine && !pairedLeft.isAfterLambdaArgumentOnSameLine()) { expectedIndent-- + debug { "--on(${n.elementType}) -> $expectedIndent" } + val byKeywordOnSameLine = pairedLeft?.prevLeafOnSameLine(BY_KEYWORD) if (byKeywordOnSameLine != null && byKeywordOnSameLine.prevLeaf()?.isWhiteSpaceWithNewline() == true && n.leavesOnSameLine(forward = true).all { it.isWhiteSpace() || it.isPartOfComment() } - ) expectedIndent-- - debug { "--${n.text} -> $expectedIndent" } + ) { + expectedIndent-- + debug { "--on same line as by keyword ${n.text} -> $expectedIndent" } + } } } LT -> @@ -641,6 +645,7 @@ class IndentationRule : Rule("indent"), Rule.Modifier.RestrictToRootLast { visitWhiteSpace(n, autoCorrect, emit, editorConfig) if (ctx.localAdj != 0) { expectedIndent += ctx.localAdj + debug { "++${ctx.localAdj} on whitespace containing new line (${n.elementType}) -> $expectedIndent" } ctx.localAdj = 0 } } else if (n.isPartOf(KDOC)) { @@ -734,8 +739,15 @@ class IndentationRule : Rule("indent"), Rule.Modifier.RestrictToRootLast { } private fun adjustExpectedIndentAfterSuperTypeList(n: ASTNode) { - expectedIndent-- - debug { "--after(${n.elementType}) -> $expectedIndent" } + val byKeywordLeaf = n + .findChildByType(DELEGATED_SUPER_TYPE_ENTRY) + ?.findChildByType(BY_KEYWORD) + if (n.prevLeaf()?.textContains('\n') == true && byKeywordLeaf?.prevLeaf().isWhiteSpaceWithNewline()) { + Unit + } else { + expectedIndent-- + debug { "--after(${n.elementType}) -> $expectedIndent" } + } } private fun adjustExpectedIndentInsideSuperTypeCall(n: ASTNode, ctx: IndentContext) { @@ -997,13 +1009,17 @@ class IndentationRule : Rule("indent"), Rule.Modifier.RestrictToRootLast { // instead of expected // val i: Int // by lazy { 1 } - nextLeafElementType == BY_KEYWORD -> - if (node.isPartOf(DELEGATED_SUPER_TYPE_ENTRY)) { + nextLeafElementType == BY_KEYWORD -> { + if (node.isPartOf(DELEGATED_SUPER_TYPE_ENTRY) && + node.treeParent.prevLeaf()?.textContains('\n') == true + ) { 0 } else { expectedIndent++ + debug { "++whitespace followed by BY keyword -> $expectedIndent" } 1 } + } // IDEA quirk: // var value: DataClass = // DataClass("too long line") diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt index 281d1a49e8..f1ce955463 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt @@ -952,23 +952,42 @@ internal class IndentationRuleTest { } @Test - fun `lint delegation 2`() { - assertThat( - IndentationRule().lint( - """ - interface Foo + fun `lint and format delegation 2`() { + val code = + """ + interface Foo - class Bar(a: Int, b: Int, c: Int) : Foo + class Bar(a: Int, b: Int, c: Int) : Foo + + class Test2 : Foo + by Bar( + a = 1, + b = 2, + c = 3 + ) + """.trimIndent() + val formattedCode = + """ + interface Foo - class Test2 : Foo + class Bar(a: Int, b: Int, c: Int) : Foo + + class Test2 : Foo by Bar( a = 1, b = 2, c = 3 ) - """.trimIndent() - ) - ).isEmpty() + """.trimIndent() + + assertThat(IndentationRule().lint(code)).containsExactly( + LintError(6, 1, "indent", "Unexpected indentation (0) (should be 4)"), + LintError(7, 1, "indent", "Unexpected indentation (4) (should be 8)"), + LintError(8, 1, "indent", "Unexpected indentation (4) (should be 8)"), + LintError(9, 1, "indent", "Unexpected indentation (4) (should be 8)"), + LintError(10, 1, "indent", "Unexpected indentation (0) (should be 4)"), + ) + assertThat(IndentationRule().format(code)).isEqualTo(formattedCode) } @Test @@ -1556,6 +1575,67 @@ internal class IndentationRuleTest { ) } + @Test + fun `Issue 1210 - format supertype delegate`() { + val code = + """ + object ApplicationComponentFactory : ApplicationComponent.Factory + by DaggerApplicationComponent.factory() + """.trimIndent() + val formattedCode = + """ + object ApplicationComponentFactory : ApplicationComponent.Factory + by DaggerApplicationComponent.factory() + """.trimIndent() + assertThat(IndentationRule().lint(code)).containsExactly( + LintError(2, 1, "indent", "Unexpected indentation (0) (should be 4)") + ) + assertThat(IndentationRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Issue 1210 - format of statements after supertype delegated entry 1`() { + val code = + """ + object Issue1210 : ApplicationComponent.Factory + by DaggerApplicationComponent.factory() + + // The next line ensures that the fix regarding the expectedIndex due to alignment of "by" keyword in + // class above, is still in place. Without this fix, the expectedIndex would hold a negative value, + // resulting in the formatting to crash on the next line. + val bar = 1 + """.trimIndent() + + assertThat(IndentationRule().lint(code)).isEmpty() + assertThat(IndentationRule().format(code)).isEqualTo(code) + } + + @Test + fun `Issue 1210 - format of statements after supertype delegated entry 2`() { + val code = + """ + interface Foo + + class Bar(a: Int, b: Int, c: Int) : Foo + + class Test4 : + Foo + by Bar( + a = 1, + b = 2, + c = 3 + ) + + // The next line ensures that the fix regarding the expectedIndex due to alignment of "by" keyword in + // class above, is still in place. Without this fix, the expectedIndex would hold a negative value, + // resulting in the formatting to crash on the next line. + val bar = 1 + """.trimIndent() + + assertThat(IndentationRule().lint(code)).isEmpty() + assertThat(IndentationRule().format(code)).isEqualTo(code) + } + private companion object { const val MULTILINE_STRING_QUOTE = "${'"'}${'"'}${'"'}" const val TAB = "${'\t'}"