From 7393cb858242fb31dec0e974b2d81df7ed011dbd Mon Sep 17 00:00:00 2001 From: ao0000 Date: Mon, 17 Apr 2023 09:25:51 +0900 Subject: [PATCH 01/80] Add new rule don't allow empty files --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt new file mode 100644 index 0000000000..e597ad15ff --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -0,0 +1,57 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.rule.engine.core.api.ElementType +import com.pinterest.ktlint.rule.engine.core.api.Rule +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty +import com.pinterest.ktlint.ruleset.standard.StandardRule +import org.ec4j.core.model.PropertyType +import org.jetbrains.kotlin.com.intellij.lang.ASTNode + +public class NoEmptyFileRule : + StandardRule(id = "no-empty-file", usesEditorConfigProperties = setOf(NO_EMPTY_FILE_PROPERTY)), + Rule.Experimental { + private var noEmptyFile = NO_EMPTY_FILE_PROPERTY.defaultValue + + override fun beforeFirstNode(editorConfig: EditorConfig) { + noEmptyFile = editorConfig[NO_EMPTY_FILE_PROPERTY] + } + + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + node + .takeIf { it.elementType == ElementType.FILE } + ?.takeIf { it.textLength == TEXT_LENGTH_EMPTY_FILE_CONTAINS } + ?.let { + val filePath = it.psi.containingFile.virtualFile.name + val fileName = + filePath + .replace("\\", "/") // Ensure compatibility with Windows OS + .substringAfterLast("/") + emit( + 0, + "File `$fileName` should not be empty", + false, + ) + } + } + + public companion object { + private const val TEXT_LENGTH_EMPTY_FILE_CONTAINS: Int = 0 + + private val NO_EMPTY_FILE_PROPERTY: EditorConfigProperty = + EditorConfigProperty( + type = + PropertyType.LowerCasingPropertyType( + "no_empty_file", + "", + PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, + setOf(true.toString(), false.toString()), + ), + defaultValue = true, + ) + } +} From 57608a7ccf397e414270fe2efa937fcfdfdb9f6c Mon Sep 17 00:00:00 2001 From: ao0000 Date: Mon, 17 Apr 2023 09:28:37 +0900 Subject: [PATCH 02/80] Set to standard rule provider --- .../ktlint/ruleset/standard/StandardRuleSetProvider.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt index 6496a9da29..74694391ff 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt @@ -39,6 +39,7 @@ import com.pinterest.ktlint.ruleset.standard.rules.NoBlankLinesInChainedMethodCa import com.pinterest.ktlint.ruleset.standard.rules.NoConsecutiveBlankLinesRule import com.pinterest.ktlint.ruleset.standard.rules.NoConsecutiveCommentsRule import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyClassBodyRule +import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyFileRule import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyFirstLineInClassBodyRule import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyFirstLineInMethodBlockRule import com.pinterest.ktlint.ruleset.standard.rules.NoLineBreakAfterElseRule @@ -119,6 +120,7 @@ public class StandardRuleSetProvider : RuleProvider { NoBlankLinesInChainedMethodCallsRule() }, RuleProvider { NoConsecutiveBlankLinesRule() }, RuleProvider { NoConsecutiveCommentsRule() }, + RuleProvider { NoEmptyFileRule() }, RuleProvider { NoEmptyClassBodyRule() }, RuleProvider { NoEmptyFirstLineInClassBodyRule() }, RuleProvider { NoEmptyFirstLineInMethodBlockRule() }, From 8e5da5add4c841b2c20800091920952ffc09ce72 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Mon, 17 Apr 2023 10:35:36 +0900 Subject: [PATCH 03/80] Refactor editorconfig property description --- .../pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index e597ad15ff..9f1214812b 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -47,7 +47,7 @@ public class NoEmptyFileRule : type = PropertyType.LowerCasingPropertyType( "no_empty_file", - "", + "Define whether empty files are allowed", PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, setOf(true.toString(), false.toString()), ), From f091560dc6e3040f3c0476d5e7d14b413a8e7cdd Mon Sep 17 00:00:00 2001 From: ao0000 Date: Tue, 18 Apr 2023 21:02:01 +0900 Subject: [PATCH 04/80] Add test for no empty file rule --- .../standard/rules/NoEmptyFileRuleTest.kt | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt new file mode 100644 index 0000000000..0a17727ad1 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -0,0 +1,43 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule +import org.junit.jupiter.api.Test + +class NoEmptyFileRuleTest { + private val noEmptyFileRuleAssertThat = assertThatRule { NoEmptyFileRule() } + + @Test + fun `Given not empty kotlin file then ignore the rule for this file`() { + val code = """ + package tmp + """.trimIndent() + + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .hasNoLintViolations() + } + + @Test + fun `Given an empty kotlin file then do a return lint error`() { + val fileName = "Tmp.kt" + val code = """ + + """.trimIndent() + + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/$fileName") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `$fileName` should not be empty") + } + + @Test + fun `Given an empty kotlin script file then do a return lint error`() { + val fileName = "Tmp.kts" + val code = """ + + """.trimIndent() + + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/$fileName") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `$fileName` should not be empty") + } +} From b1f2f552d3e3a7fd9cf9777587da2ac2e5cc4804 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Wed, 19 Apr 2023 08:05:39 +0900 Subject: [PATCH 05/80] Add to support editorconfig settings --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 9f1214812b..4a12c822c8 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -22,21 +22,23 @@ public class NoEmptyFileRule : autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, ) { - node - .takeIf { it.elementType == ElementType.FILE } - ?.takeIf { it.textLength == TEXT_LENGTH_EMPTY_FILE_CONTAINS } - ?.let { - val filePath = it.psi.containingFile.virtualFile.name - val fileName = - filePath - .replace("\\", "/") // Ensure compatibility with Windows OS - .substringAfterLast("/") - emit( - 0, - "File `$fileName` should not be empty", - false, - ) - } + if (noEmptyFile) { + node + .takeIf { it.elementType == ElementType.FILE } + ?.takeIf { it.textLength == TEXT_LENGTH_EMPTY_FILE_CONTAINS } + ?.let { + val filePath = it.psi.containingFile.virtualFile.name + val fileName = + filePath + .replace("\\", "/") // Ensure compatibility with Windows OS + .substringAfterLast("/") + emit( + 0, + "File `$fileName` should not be empty", + false, + ) + } + } } public companion object { From cfa3169889a2825b400e5d36859971cca88bb573 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Wed, 19 Apr 2023 08:07:51 +0900 Subject: [PATCH 06/80] Add test for supporting editorconfig --- .../ruleset/standard/rules/NoEmptyFileRuleTest.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index 0a17727ad1..d748518402 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -40,4 +40,16 @@ class NoEmptyFileRuleTest { .asFileWithPath("/some/path/$fileName") .hasLintViolationWithoutAutoCorrect(1, 1, "File `$fileName` should not be empty") } + + @Test + fun testLintOff() { + val code = + """ + + """.trimIndent() + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to false) + .hasNoLintViolations() + } } From 20bae7fbb06d484f70e9bbc69746cc2d146fa8c6 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Wed, 19 Apr 2023 08:08:54 +0900 Subject: [PATCH 07/80] Fix default setting and remove rule experimental --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 8 +++---- .../standard/rules/NoEmptyFileRuleTest.kt | 22 +++++++++++-------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 4a12c822c8..5209ebfbe2 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -1,7 +1,6 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.ElementType -import com.pinterest.ktlint.rule.engine.core.api.Rule import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty import com.pinterest.ktlint.ruleset.standard.StandardRule @@ -9,8 +8,7 @@ import org.ec4j.core.model.PropertyType import org.jetbrains.kotlin.com.intellij.lang.ASTNode public class NoEmptyFileRule : - StandardRule(id = "no-empty-file", usesEditorConfigProperties = setOf(NO_EMPTY_FILE_PROPERTY)), - Rule.Experimental { + StandardRule(id = "no-empty-file", usesEditorConfigProperties = setOf(NO_EMPTY_FILE_PROPERTY)) { private var noEmptyFile = NO_EMPTY_FILE_PROPERTY.defaultValue override fun beforeFirstNode(editorConfig: EditorConfig) { @@ -44,7 +42,7 @@ public class NoEmptyFileRule : public companion object { private const val TEXT_LENGTH_EMPTY_FILE_CONTAINS: Int = 0 - private val NO_EMPTY_FILE_PROPERTY: EditorConfigProperty = + public val NO_EMPTY_FILE_PROPERTY: EditorConfigProperty = EditorConfigProperty( type = PropertyType.LowerCasingPropertyType( @@ -53,7 +51,7 @@ public class NoEmptyFileRule : PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, setOf(true.toString(), false.toString()), ), - defaultValue = true, + defaultValue = false, ) } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index d748518402..169b064ea5 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -1,5 +1,6 @@ package com.pinterest.ktlint.ruleset.standard.rules +import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyFileRule.Companion.NO_EMPTY_FILE_PROPERTY import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule import org.junit.jupiter.api.Test @@ -8,36 +9,39 @@ class NoEmptyFileRuleTest { @Test fun `Given not empty kotlin file then ignore the rule for this file`() { - val code = """ + val code = + """ package tmp - """.trimIndent() - + """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") + .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasNoLintViolations() } @Test fun `Given an empty kotlin file then do a return lint error`() { val fileName = "Tmp.kt" - val code = """ - - """.trimIndent() + val code = + """ + """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/$fileName") + .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasLintViolationWithoutAutoCorrect(1, 1, "File `$fileName` should not be empty") } @Test fun `Given an empty kotlin script file then do a return lint error`() { val fileName = "Tmp.kts" - val code = """ - - """.trimIndent() + val code = + """ + """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/$fileName") + .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasLintViolationWithoutAutoCorrect(1, 1, "File `$fileName` should not be empty") } From 3239077b2150756bdfb7ca4a9df2150e4218fbc3 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Wed, 19 Apr 2023 08:37:55 +0900 Subject: [PATCH 08/80] Update documents about no empty file rule --- docs/rules/configuration-ktlint.md | 8 ++++++++ docs/rules/standard.md | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/docs/rules/configuration-ktlint.md b/docs/rules/configuration-ktlint.md index 1084faf6fb..400ffd69fe 100644 --- a/docs/rules/configuration-ktlint.md +++ b/docs/rules/configuration-ktlint.md @@ -250,3 +250,11 @@ ktlint_standard_indent = disabled ``` Note that the `import-ordering` rule is disabled for *all* packages including the `api` sub package. Next to this the `indent` rule is disabled for the `api` package and its sub packages. + +## No empty file + +By default, empty files are allowed to exist. You can set to allow them not to exist. +```ini +[*.{kt,kts}] + no_empty_file = true # Use "true" if empty files are not allowed +``` diff --git a/docs/rules/standard.md b/docs/rules/standard.md index d0653e8e63..7d76e934f1 100644 --- a/docs/rules/standard.md +++ b/docs/rules/standard.md @@ -444,6 +444,10 @@ Rule id: `no-consecutive-blank-lines` Rule id: `no-empty-class-body` +## No empty files +Ensure that no empty Kotlin or Kotlin Script files are allowed. +This rule can be configured with `.editorconfig` property [`no_empty_file`](../configuration-ktlint/#no-empty-file) + ## No leading empty lines in method blocks === "[:material-heart:](#) Ktlint" From 4b4c0fcac803568015f61c406514d3a4e4fbc1f1 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sat, 22 Apr 2023 09:03:06 +0900 Subject: [PATCH 09/80] Modify to experimental rule --- .../ktlint/ruleset/standard/rules/NoEmptyFileRule.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 5209ebfbe2..415a9ce57c 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -1,6 +1,7 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.ElementType +import com.pinterest.ktlint.rule.engine.core.api.Rule import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty import com.pinterest.ktlint.ruleset.standard.StandardRule @@ -8,7 +9,8 @@ import org.ec4j.core.model.PropertyType import org.jetbrains.kotlin.com.intellij.lang.ASTNode public class NoEmptyFileRule : - StandardRule(id = "no-empty-file", usesEditorConfigProperties = setOf(NO_EMPTY_FILE_PROPERTY)) { + StandardRule(id = "no-empty-file", usesEditorConfigProperties = setOf(NO_EMPTY_FILE_PROPERTY)), + Rule.Experimental { private var noEmptyFile = NO_EMPTY_FILE_PROPERTY.defaultValue override fun beforeFirstNode(editorConfig: EditorConfig) { From 34cb7a35f1a7c0d5859f3225bd5ca8b98b6b4176 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sat, 22 Apr 2023 09:03:56 +0900 Subject: [PATCH 10/80] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e86c1d9467..709f2d5ce5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -280,6 +280,7 @@ if (node.isRoot()) { ``` * Add new experimental rule `if-else-wrapping` for `ktlint_official` code style. This enforces that a single line if-statement is kept simple. A single line if-statement may contain no more than one else-branch. The branches a single line if-statement may not be wrapped in a block. This rule can also be run for other code styles, but then it needs to be enabled explicitly. ([#812](https://github.com/pinterest/ktlint/issues/812)) * Add new experimental rule `enum-wrapping` for all code styles. An enum should either be a single line, or each enum entry should be defined on a separate line. ([#1903](https://github.com/pinterest/ktlint/issues/1903)) +* Add new experimental rule `no-empty-file` for all code styles. Kotlin and Kotlin Script empty files should not be existed. ### Removed From 91e4d9c3bff46ca8ec9b6e32e50a4b7713bbec41 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 08:28:43 +0900 Subject: [PATCH 11/80] Fix CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aed62ffe9..f551d8b09e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). ## Unreleased +* Add new experimental rule `no-empty-file` for all code styles. Kotlin and Kotlin Scription files may not be empty. ### Added @@ -297,7 +298,6 @@ if (node.isRoot()) { } ``` * Add new experimental rule `enum-wrapping` for all code styles. An enum should either be a single line, or each enum entry should be defined on a separate line. ([#1903](https://github.com/pinterest/ktlint/issues/1903)) -* Add new experimental rule `no-empty-file` for all code styles. Kotlin and Kotlin Script empty files should not be existed. ### Removed From de734117b89b0b6f6d2cff9acc9d7d63c2a69a85 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 08:32:16 +0900 Subject: [PATCH 12/80] Remove no empty files section in docs --- docs/rules/configuration-ktlint.md | 8 -------- docs/rules/standard.md | 4 ---- 2 files changed, 12 deletions(-) diff --git a/docs/rules/configuration-ktlint.md b/docs/rules/configuration-ktlint.md index 44725c4070..891d08843a 100644 --- a/docs/rules/configuration-ktlint.md +++ b/docs/rules/configuration-ktlint.md @@ -253,11 +253,3 @@ ktlint_standard_indent = disabled ``` Note that the `import-ordering` rule is disabled for *all* packages including the `api` sub package. Next to this the `indent` rule is disabled for the `api` package and its sub packages. - -## No empty file - -By default, empty files are allowed to exist. You can set to allow them not to exist. -```ini -[*.{kt,kts}] - no_empty_file = true # Use "true" if empty files are not allowed -``` diff --git a/docs/rules/standard.md b/docs/rules/standard.md index 9be2d3dffc..12ff505919 100644 --- a/docs/rules/standard.md +++ b/docs/rules/standard.md @@ -444,10 +444,6 @@ Rule id: `no-consecutive-blank-lines` Rule id: `no-empty-class-body` -## No empty files -Ensure that no empty Kotlin or Kotlin Script files are allowed. -This rule can be configured with `.editorconfig` property [`no_empty_file`](../configuration-ktlint/#no-empty-file) - ## No leading empty lines in method blocks === "[:material-heart:](#) Ktlint" From 349c2e4aa697e3e349c822419850022a209c2ba0 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 08:36:33 +0900 Subject: [PATCH 13/80] Change to alphabetical order --- .../ktlint/ruleset/standard/StandardRuleSetProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt index 74694391ff..5cbe423c56 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt @@ -120,8 +120,8 @@ public class StandardRuleSetProvider : RuleProvider { NoBlankLinesInChainedMethodCallsRule() }, RuleProvider { NoConsecutiveBlankLinesRule() }, RuleProvider { NoConsecutiveCommentsRule() }, - RuleProvider { NoEmptyFileRule() }, RuleProvider { NoEmptyClassBodyRule() }, + RuleProvider { NoEmptyFileRule() }, RuleProvider { NoEmptyFirstLineInClassBodyRule() }, RuleProvider { NoEmptyFirstLineInMethodBlockRule() }, RuleProvider { NoLineBreakAfterElseRule() }, From 152fec376f57b2a3f4db45646f14b79a657a19ed Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 09:46:34 +0900 Subject: [PATCH 14/80] Refactor NoEmptyFileRule --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 415a9ce57c..c241fb644f 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -25,25 +25,16 @@ public class NoEmptyFileRule : if (noEmptyFile) { node .takeIf { it.elementType == ElementType.FILE } - ?.takeIf { it.textLength == TEXT_LENGTH_EMPTY_FILE_CONTAINS } + ?.takeIf { it.text.isBlank() } ?.let { val filePath = it.psi.containingFile.virtualFile.name - val fileName = - filePath - .replace("\\", "/") // Ensure compatibility with Windows OS - .substringAfterLast("/") - emit( - 0, - "File `$fileName` should not be empty", - false, - ) + .replace("\\", "/") // Ensure compatibility with Windows OS + emit(0, "File `$filePath` should not be empty", false) } } } public companion object { - private const val TEXT_LENGTH_EMPTY_FILE_CONTAINS: Int = 0 - public val NO_EMPTY_FILE_PROPERTY: EditorConfigProperty = EditorConfigProperty( type = From 775fa47f0d9b4d0c8d16cbb10ad0654ad6af491e Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 09:49:02 +0900 Subject: [PATCH 15/80] Fix to enable no_empty_file rule on default to reduce .editorconfig --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index c241fb644f..904651ebaf 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -38,13 +38,13 @@ public class NoEmptyFileRule : public val NO_EMPTY_FILE_PROPERTY: EditorConfigProperty = EditorConfigProperty( type = - PropertyType.LowerCasingPropertyType( - "no_empty_file", - "Define whether empty files are allowed", - PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, - setOf(true.toString(), false.toString()), - ), - defaultValue = false, + PropertyType.LowerCasingPropertyType( + "no_empty_file", + "Define whether empty files are allowed", + PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, + setOf(true.toString(), false.toString()), + ), + defaultValue = true, ) } } From 2ce9573d4694b94fc3bdf96b1b1e0f9942819661 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 10:27:31 +0900 Subject: [PATCH 16/80] Fix NoEmptyFileRuleTest --- .../standard/rules/NoEmptyFileRuleTest.kt | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index 169b064ea5..8b0f2c0d33 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -8,10 +8,13 @@ class NoEmptyFileRuleTest { private val noEmptyFileRuleAssertThat = assertThatRule { NoEmptyFileRule() } @Test - fun `Given not empty kotlin file then ignore the rule for this file`() { + fun `Given non-empty kotlin file then ignore the rule for this file`() { val code = """ package tmp + fun main(){ + println("Hello world") + } """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") @@ -21,39 +24,32 @@ class NoEmptyFileRuleTest { @Test fun `Given an empty kotlin file then do a return lint error`() { - val fileName = "Tmp.kt" - val code = - """ - - """.trimIndent() + val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) - .asFileWithPath("/some/path/$fileName") + .asFileWithPath("/some/path/Tmp.kt") .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) - .hasLintViolationWithoutAutoCorrect(1, 1, "File `$fileName` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } @Test fun `Given an empty kotlin script file then do a return lint error`() { - val fileName = "Tmp.kts" - val code = - """ - - """.trimIndent() + val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) - .asFileWithPath("/some/path/$fileName") + .asFileWithPath("/some/path/Tmp.kts") .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) - .hasLintViolationWithoutAutoCorrect(1, 1, "File `$fileName` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kts` should not be empty") } @Test - fun testLintOff() { - val code = - """ - - """.trimIndent() + fun `Given empty kotlin file when lint disable then ignore the rule for this file`() { + val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to false) .hasNoLintViolations() } + + private companion object { + private const val EMPTY_FILE = "" + } } From 3632568b2afc7530ad096ea0a6849e74d973b196 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 10:29:11 +0900 Subject: [PATCH 17/80] Add non-empty kotlin file version test --- .../ruleset/standard/rules/NoEmptyFileRuleTest.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index 8b0f2c0d33..acfa06c38d 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -49,6 +49,20 @@ class NoEmptyFileRuleTest { .hasNoLintViolations() } + @Test + fun `Given non-empty kotlin file when lint disable then ignore this file`() { + val code = """ + package tmp + fun main(){ + println("Hello world") + } + """.trimIndent() + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to false) + .hasNoLintViolations() + } + private companion object { private const val EMPTY_FILE = "" } From 1ad2609a4504c0ddf25f49916ec0796a71970974 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 10:31:31 +0900 Subject: [PATCH 18/80] Format --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 17 +++++++++-------- .../standard/rules/NoEmptyFileRuleTest.kt | 5 +++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 904651ebaf..82c57d8430 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -27,8 +27,9 @@ public class NoEmptyFileRule : .takeIf { it.elementType == ElementType.FILE } ?.takeIf { it.text.isBlank() } ?.let { - val filePath = it.psi.containingFile.virtualFile.name - .replace("\\", "/") // Ensure compatibility with Windows OS + val filePath = + it.psi.containingFile.virtualFile.name + .replace("\\", "/") // Ensure compatibility with Windows OS emit(0, "File `$filePath` should not be empty", false) } } @@ -38,12 +39,12 @@ public class NoEmptyFileRule : public val NO_EMPTY_FILE_PROPERTY: EditorConfigProperty = EditorConfigProperty( type = - PropertyType.LowerCasingPropertyType( - "no_empty_file", - "Define whether empty files are allowed", - PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, - setOf(true.toString(), false.toString()), - ), + PropertyType.LowerCasingPropertyType( + "no_empty_file", + "Define whether empty files are allowed", + PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, + setOf(true.toString(), false.toString()), + ), defaultValue = true, ) } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index acfa06c38d..dd988e98f6 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -51,12 +51,13 @@ class NoEmptyFileRuleTest { @Test fun `Given non-empty kotlin file when lint disable then ignore this file`() { - val code = """ + val code = + """ package tmp fun main(){ println("Hello world") } - """.trimIndent() + """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to false) From 13403b4d6c121d921575c615d231af6b2e019eb8 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Wed, 26 Apr 2023 23:10:50 +0900 Subject: [PATCH 19/80] Add empty file case for package or import statements only --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 82c57d8430..fc88c3fc5b 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -2,8 +2,11 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.ElementType import com.pinterest.ktlint.rule.engine.core.api.Rule +import com.pinterest.ktlint.rule.engine.core.api.children import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty +import com.pinterest.ktlint.rule.engine.core.api.isRoot +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace import com.pinterest.ktlint.ruleset.standard.StandardRule import org.ec4j.core.model.PropertyType import org.jetbrains.kotlin.com.intellij.lang.ASTNode @@ -22,17 +25,30 @@ public class NoEmptyFileRule : autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, ) { - if (noEmptyFile) { - node - .takeIf { it.elementType == ElementType.FILE } - ?.takeIf { it.text.isBlank() } - ?.let { - val filePath = - it.psi.containingFile.virtualFile.name - .replace("\\", "/") // Ensure compatibility with Windows OS - emit(0, "File `$filePath` should not be empty", false) - } - } + if (!noEmptyFile) return + + node + .takeIf { it.isRoot() } + ?.takeIf { it.isEmptyFile() } + ?.let { + val filePath = + node.psi.containingFile.virtualFile.name + .replace("\\", "/") // Ensure compatibility with Windows OS + emit(0, "File `$filePath` should not be empty", false) + } + } + + private fun ASTNode.isEmptyFile(): Boolean { + if (text.isBlank()) return true + + return this.children() + .toList() + .filter { + !it.isWhiteSpace() && + it.elementType != ElementType.PACKAGE_DIRECTIVE && + it.elementType != ElementType.IMPORT_LIST + } + .isEmpty() } public companion object { From 14614c119613415dacc98a79e9871dd2eef1541a Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sat, 29 Apr 2023 09:52:53 +0900 Subject: [PATCH 20/80] Move comment to unreleased added section in CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f551d8b09e..05a4a043b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,9 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). ## Unreleased -* Add new experimental rule `no-empty-file` for all code styles. Kotlin and Kotlin Scription files may not be empty. ### Added +* Add new experimental rule `no-empty-file` for all code styles. Kotlin and Kotlin Scription files may not be empty. ### Removed From 3fa16cdcf5848b9863ac12f0bfef415a8b2ad3b1 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sat, 29 Apr 2023 09:54:57 +0900 Subject: [PATCH 21/80] Add test case about only package and only import statement --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 3 +-- .../standard/rules/NoEmptyFileRuleTest.kt | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index fc88c3fc5b..3f46fcc0f7 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -47,8 +47,7 @@ public class NoEmptyFileRule : !it.isWhiteSpace() && it.elementType != ElementType.PACKAGE_DIRECTIVE && it.elementType != ElementType.IMPORT_LIST - } - .isEmpty() + }.isEmpty() } public companion object { diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index dd988e98f6..9726f50e9e 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -40,6 +40,32 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kts` should not be empty") } + @Test + fun `Given only package statement in kotlin file then do a return lint error`() { + val code = + """ + package path + """.trimIndent() + + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) + .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") + } + + @Test + fun `Given only import statement in kotlin file then do a return lint error`() { + val code = + """ + package path + import sample.Hello + """.trimIndent() + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) + .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") + } + @Test fun `Given empty kotlin file when lint disable then ignore the rule for this file`() { val code = EMPTY_FILE From 497fcf6b23385ca80b1060ae96927108df91be0a Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 30 Apr 2023 23:17:51 +0900 Subject: [PATCH 22/80] Delete editorconfig property --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 29 +------------------ .../standard/rules/NoEmptyFileRuleTest.kt | 18 +----------- 2 files changed, 2 insertions(+), 45 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 3f46fcc0f7..00389f9380 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -3,30 +3,17 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.ElementType import com.pinterest.ktlint.rule.engine.core.api.Rule import com.pinterest.ktlint.rule.engine.core.api.children -import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig -import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty import com.pinterest.ktlint.rule.engine.core.api.isRoot import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace import com.pinterest.ktlint.ruleset.standard.StandardRule -import org.ec4j.core.model.PropertyType import org.jetbrains.kotlin.com.intellij.lang.ASTNode -public class NoEmptyFileRule : - StandardRule(id = "no-empty-file", usesEditorConfigProperties = setOf(NO_EMPTY_FILE_PROPERTY)), - Rule.Experimental { - private var noEmptyFile = NO_EMPTY_FILE_PROPERTY.defaultValue - - override fun beforeFirstNode(editorConfig: EditorConfig) { - noEmptyFile = editorConfig[NO_EMPTY_FILE_PROPERTY] - } - +public class NoEmptyFileRule : StandardRule(id = "no-empty-file"), Rule.Experimental { override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, ) { - if (!noEmptyFile) return - node .takeIf { it.isRoot() } ?.takeIf { it.isEmptyFile() } @@ -49,18 +36,4 @@ public class NoEmptyFileRule : it.elementType != ElementType.IMPORT_LIST }.isEmpty() } - - public companion object { - public val NO_EMPTY_FILE_PROPERTY: EditorConfigProperty = - EditorConfigProperty( - type = - PropertyType.LowerCasingPropertyType( - "no_empty_file", - "Define whether empty files are allowed", - PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, - setOf(true.toString(), false.toString()), - ), - defaultValue = true, - ) - } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index 9726f50e9e..0a34c726b8 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -1,6 +1,5 @@ package com.pinterest.ktlint.ruleset.standard.rules -import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyFileRule.Companion.NO_EMPTY_FILE_PROPERTY import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule import org.junit.jupiter.api.Test @@ -18,7 +17,6 @@ class NoEmptyFileRuleTest { """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasNoLintViolations() } @@ -27,7 +25,6 @@ class NoEmptyFileRuleTest { val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } @@ -36,7 +33,6 @@ class NoEmptyFileRuleTest { val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kts") - .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kts` should not be empty") } @@ -49,7 +45,6 @@ class NoEmptyFileRuleTest { noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } @@ -62,21 +57,11 @@ class NoEmptyFileRuleTest { """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } @Test - fun `Given empty kotlin file when lint disable then ignore the rule for this file`() { - val code = EMPTY_FILE - noEmptyFileRuleAssertThat(code) - .asFileWithPath("/some/path/Tmp.kt") - .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to false) - .hasNoLintViolations() - } - - @Test - fun `Given non-empty kotlin file when lint disable then ignore this file`() { + fun `Given non-empty kotlin file then ignore this file`() { val code = """ package tmp @@ -86,7 +71,6 @@ class NoEmptyFileRuleTest { """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to false) .hasNoLintViolations() } From 504209e9c771ac8cc083db6d8902f2b55c56d838 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 30 Apr 2023 23:27:00 +0900 Subject: [PATCH 23/80] Add test case about only import statement --- .../ruleset/standard/rules/NoEmptyFileRuleTest.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index 0a34c726b8..fe4fd062cd 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -50,6 +50,17 @@ class NoEmptyFileRuleTest { @Test fun `Given only import statement in kotlin file then do a return lint error`() { + val code = + """ + import sample.Hello + """.trimIndent() + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") + } + + @Test + fun `Given only package and import statements in kotlin file then do a return lint error`() { val code = """ package path From e3598798742a02f70f4896fb538113e92ddc9e4f Mon Sep 17 00:00:00 2001 From: ao0000 Date: Mon, 1 May 2023 09:42:46 +0900 Subject: [PATCH 24/80] Disable test case related on Windows OS file system --- .../ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index fe4fd062cd..23f8480531 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -2,6 +2,8 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.DisabledOnOs +import org.junit.jupiter.api.condition.OS class NoEmptyFileRuleTest { private val noEmptyFileRuleAssertThat = assertThatRule { NoEmptyFileRule() } @@ -20,6 +22,7 @@ class NoEmptyFileRuleTest { .hasNoLintViolations() } + @DisabledOnOs(OS.WINDOWS) @Test fun `Given an empty kotlin file then do a return lint error`() { val code = EMPTY_FILE @@ -28,6 +31,7 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } + @DisabledOnOs(OS.WINDOWS) @Test fun `Given an empty kotlin script file then do a return lint error`() { val code = EMPTY_FILE @@ -36,6 +40,7 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kts` should not be empty") } + @DisabledOnOs(OS.WINDOWS) @Test fun `Given only package statement in kotlin file then do a return lint error`() { val code = @@ -48,6 +53,7 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } + @DisabledOnOs(OS.WINDOWS) @Test fun `Given only import statement in kotlin file then do a return lint error`() { val code = @@ -59,6 +65,7 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } + @DisabledOnOs(OS.WINDOWS) @Test fun `Given only package and import statements in kotlin file then do a return lint error`() { val code = From f22e5222aa611b386c18ef69a830a134a6aa0310 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Mon, 1 May 2023 19:55:06 +0900 Subject: [PATCH 25/80] Revert "Disable test case related on Windows OS file system" This reverts commit e3598798742a02f70f4896fb538113e92ddc9e4f. --- .../ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index 23f8480531..fe4fd062cd 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -2,8 +2,6 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule import org.junit.jupiter.api.Test -import org.junit.jupiter.api.condition.DisabledOnOs -import org.junit.jupiter.api.condition.OS class NoEmptyFileRuleTest { private val noEmptyFileRuleAssertThat = assertThatRule { NoEmptyFileRule() } @@ -22,7 +20,6 @@ class NoEmptyFileRuleTest { .hasNoLintViolations() } - @DisabledOnOs(OS.WINDOWS) @Test fun `Given an empty kotlin file then do a return lint error`() { val code = EMPTY_FILE @@ -31,7 +28,6 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } - @DisabledOnOs(OS.WINDOWS) @Test fun `Given an empty kotlin script file then do a return lint error`() { val code = EMPTY_FILE @@ -40,7 +36,6 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kts` should not be empty") } - @DisabledOnOs(OS.WINDOWS) @Test fun `Given only package statement in kotlin file then do a return lint error`() { val code = @@ -53,7 +48,6 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } - @DisabledOnOs(OS.WINDOWS) @Test fun `Given only import statement in kotlin file then do a return lint error`() { val code = @@ -65,7 +59,6 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } - @DisabledOnOs(OS.WINDOWS) @Test fun `Given only package and import statements in kotlin file then do a return lint error`() { val code = From 2ef3a3488c698a3f53f4611453dc41eb9529ab80 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Mon, 1 May 2023 20:13:02 +0900 Subject: [PATCH 26/80] Fix lint error to use file name --- .../ktlint/ruleset/standard/rules/NoEmptyFileRule.kt | 5 +++-- .../ruleset/standard/rules/NoEmptyFileRuleTest.kt | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 00389f9380..a705930f9e 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -18,10 +18,11 @@ public class NoEmptyFileRule : StandardRule(id = "no-empty-file"), Rule.Experime .takeIf { it.isRoot() } ?.takeIf { it.isEmptyFile() } ?.let { - val filePath = + val fileName = node.psi.containingFile.virtualFile.name .replace("\\", "/") // Ensure compatibility with Windows OS - emit(0, "File `$filePath` should not be empty", false) + .substringAfterLast("/") + emit(0, "File `$fileName` should not be empty", false) } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index fe4fd062cd..1aa2bc6cc3 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -25,7 +25,7 @@ class NoEmptyFileRuleTest { val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `Tmp.kt` should not be empty") } @Test @@ -33,7 +33,7 @@ class NoEmptyFileRuleTest { val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kts") - .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kts` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `Tmp.kts` should not be empty") } @Test @@ -45,7 +45,7 @@ class NoEmptyFileRuleTest { noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `Tmp.kt` should not be empty") } @Test @@ -56,7 +56,7 @@ class NoEmptyFileRuleTest { """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `Tmp.kt` should not be empty") } @Test @@ -68,7 +68,7 @@ class NoEmptyFileRuleTest { """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `Tmp.kt` should not be empty") } @Test From b69bdc5ecbb1f21e9a6d4e4bccf763100334127d Mon Sep 17 00:00:00 2001 From: ao0000 Date: Fri, 5 May 2023 00:30:10 +0900 Subject: [PATCH 27/80] Fix CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd554cd17c..b30954d2cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Unreleased ### Added -* Add new experimental rule `no-empty-file` for all code styles. Kotlin and Kotlin Scription files may not be empty. +* Add new experimental rule `no-empty-file` for all code styles. Kotlin file may not be empty. ### Removed From f408c888acbf2aa1a2bb11d7a7f32f68be9db413 Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Fri, 5 May 2023 15:36:32 +0200 Subject: [PATCH 28/80] Handle parameter `--code-style=android_studio` in Ktlint CLI identical to deprecated parameter `--android` (#1990) Closes #1982 --- CHANGELOG.md | 1 + .../ktlint/cli/internal/KtlintCommandLine.kt | 6 ++-- .../com/pinterest/ktlint/cli/SimpleCLITest.kt | 35 +++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c26c28338..6a6a2fb78a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Fixed * Store path of file containing a lint violation relative to the location of the baseline file itself ([#1962](https://github.com/pinterest/ktlint/issues/1962)) +* Handle parameter `--code-style=android_studio` in Ktlint CLI identical to deprecated parameter `--android` ([#1982](https://github.com/pinterest/ktlint/issues/1982)) ### Changed * Separated Baseline functionality out of `ktlint-cli` into separate `ktlint-baseline` module for API consumers diff --git a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt index 7307c5510e..2810e62763 100644 --- a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt +++ b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt @@ -263,9 +263,9 @@ internal class KtlintCommandLine { }.applyIf(disabledRules.isNotBlank()) { logger.debug { "Add editor config override to disable rules: '$disabledRules'" } plus(*disabledRulesEditorConfigOverrides()) - }.applyIf(android) { - logger.debug { "Add editor config override to set code style to 'android'" } - plus(CODE_STYLE_PROPERTY to CodeStyleValue.android) + }.applyIf(android || codeStyle == CodeStyleValue.android || codeStyle == CodeStyleValue.android_studio) { + logger.debug { "Add editor config override to set code style to 'android_studio'" } + plus(CODE_STYLE_PROPERTY to CodeStyleValue.android_studio) }.applyIf(stdin) { logger.debug { "Add editor config override to disable 'filename' rule which can not be used in combination with reading from " + diff --git a/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/SimpleCLITest.kt b/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/SimpleCLITest.kt index 54cec00d40..9e69ce8983 100644 --- a/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/SimpleCLITest.kt +++ b/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/SimpleCLITest.kt @@ -476,4 +476,39 @@ class SimpleCLITest { .doesNotContainLineMatching(Regex(".*Needless blank line.*")) } } + + @Nested + inner class `Issue 1983 - Enable android code style` { + @Test + fun `Enable android code style via parameter --android`( + @TempDir + tempDir: Path, + ) { + CommandLineTestRunner(tempDir) + .run( + testProjectName = "too-many-empty-lines", + arguments = listOf("--android"), + ) { + assertThat(normalOutput).containsLineMatching( + Regex(".*Add editor config override to set code style to 'android_studio'.*"), + ) + } + } + + @Test + fun `Enable android code style via parameter --code-style=android_studio`( + @TempDir + tempDir: Path, + ) { + CommandLineTestRunner(tempDir) + .run( + testProjectName = "too-many-empty-lines", + arguments = listOf("--code-style=android_studio"), + ) { + assertThat(normalOutput).containsLineMatching( + Regex(".*Add editor config override to set code style to 'android_studio'.*"), + ) + } + } + } } From 92141839052b3939664fdad95c3a5b5bca303eab Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Fri, 5 May 2023 17:55:32 +0200 Subject: [PATCH 29/80] Prevent nullpointer exception (NPE) if class without body is followed by multiple blank lines until end of file (#1991) Closes #1987 --- CHANGELOG.md | 1 + .../standard/rules/NoConsecutiveBlankLinesRule.kt | 2 +- .../standard/rules/NoConsecutiveBlankLinesRuleTest.kt | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a6a2fb78a..8e7a7f981f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Fixed * Store path of file containing a lint violation relative to the location of the baseline file itself ([#1962](https://github.com/pinterest/ktlint/issues/1962)) * Handle parameter `--code-style=android_studio` in Ktlint CLI identical to deprecated parameter `--android` ([#1982](https://github.com/pinterest/ktlint/issues/1982)) +* Prevent nullpointer exception (NPE) if class without body is followed by multiple blank lines until end of file `no-consecutive-blank-lines` ([#1987](https://github.com/pinterest/ktlint/issues/1987)) ### Changed * Separated Baseline functionality out of `ktlint-cli` into separate `ktlint-baseline` module for API consumers diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRule.kt index e215349c93..fc871a22c2 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRule.kt @@ -62,7 +62,7 @@ public class NoConsecutiveBlankLinesRule : StandardRule("no-consecutive-blank-li ?.let { prevNode -> prevNode.elementType == IDENTIFIER && prevNode.treeParent.elementType == CLASS && - this.treeNext.elementType == PRIMARY_CONSTRUCTOR + this.treeNext?.elementType == PRIMARY_CONSTRUCTOR } ?: false } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRuleTest.kt index db28d360e5..f93ee0b63a 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveBlankLinesRuleTest.kt @@ -212,4 +212,13 @@ class NoConsecutiveBlankLinesRuleTest { """.trimIndent(), ) } + + @Test + fun `Issue 1987 - Class without body but followed by multiple blank lines until end of file should not throw exception`() { + val code = "class Foo\n\n\n" + val formattedCode = "class Foo\n" + noConsecutiveBlankLinesRuleAssertThat(code) + .hasLintViolations(LintViolation(3, 1, "Needless blank line(s)")) + .isFormattedAs(formattedCode) + } } From 9f9328f499ac644b68371f90f467d2b4e7472fab Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Fri, 5 May 2023 17:56:44 +0200 Subject: [PATCH 30/80] Add comment why the configuration cache is disabled (#1992) --- gradle.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gradle.properties b/gradle.properties index 1abf786e19..53295eaf6a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,6 @@ POM_DEVELOPER_NAME=Pinterest, Inc. org.gradle.jvmargs=-Xmx4g org.gradle.parallel=true org.gradle.caching=true +# The release process of ktlint breaks whenever the configuration cache is enabled as not all gradle tasks fully supports this feature yet. +# As the configuration cache only slightly improves the build performance, it is kept disabled for now. org.gradle.configuration-cache=false From b30515d816c8ab3773406a38cdc2fcc8b4971fde Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Fri, 5 May 2023 18:53:09 +0200 Subject: [PATCH 31/80] Print absolute path of file in lint violations (#1988) * Print absolute path of file in lint violations when flag "--relative" is not specified in Ktlint CLI Closes #1983 --- CHANGELOG.md | 1 + .../ktlint/cli/internal/FileUtils.kt | 15 ++++-- .../ktlint/cli/internal/KtlintCommandLine.kt | 33 ++++++------ .../ktlint/cli/api/BaselineCLITest.kt | 50 ++++++++++++------- 4 files changed, 63 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e7a7f981f..cba7fb047b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Fixed * Store path of file containing a lint violation relative to the location of the baseline file itself ([#1962](https://github.com/pinterest/ktlint/issues/1962)) +* Print absolute path of file in lint violations when flag "--relative" is not specified in Ktlint CLI ([#1963](https://github.com/pinterest/ktlint/issues/1963)) * Handle parameter `--code-style=android_studio` in Ktlint CLI identical to deprecated parameter `--android` ([#1982](https://github.com/pinterest/ktlint/issues/1982)) * Prevent nullpointer exception (NPE) if class without body is followed by multiple blank lines until end of file `no-consecutive-blank-lines` ([#1987](https://github.com/pinterest/ktlint/issues/1987)) diff --git a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt index d90bcb3aee..81d795450d 100644 --- a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt +++ b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt @@ -23,7 +23,7 @@ import kotlin.system.measureTimeMillis private val LOGGER = KotlinLogging.logger {}.initKtLintKLogger() -private val WORK_DIR: String = File(".").canonicalPath +private val ROOT_DIR_PATH: Path = Paths.get("").toAbsolutePath() private val TILDE_REGEX = Regex("^(!)?~") private const val NEGATION_PREFIX = "!" @@ -334,9 +334,18 @@ private val onWindowsOS .getProperty("os.name") .startsWith("windows", true) +/** + * Gets the relative route of the path. Also adjusts the slashes for uniformity between file systems. + */ internal fun File.location(relative: Boolean) = if (relative) { - this.toPath().relativeToOrSelf(Path(WORK_DIR)).pathString + this + .toPath() + .relativeToOrSelf(ROOT_DIR_PATH) + .pathString + .replace(File.separatorChar, '/') } else { - this.path + this + .path + .replace(File.separatorChar, '/') } diff --git a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt index 2810e62763..36dcfb88c5 100644 --- a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt +++ b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt @@ -51,8 +51,6 @@ import java.util.concurrent.Future import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger -import kotlin.collections.ArrayList -import kotlin.collections.LinkedHashSet import kotlin.concurrent.thread import kotlin.io.path.pathString import kotlin.io.path.relativeToOrSelf @@ -409,16 +407,21 @@ internal class KtlintCommandLine { .map { it.toFile() } .takeWhile { errorNumber.get() < limit } .map { file -> - val fileName = file.toPath().relativeRoute Callable { - fileName to + file to process( ktLintRuleEngine = ktLintRuleEngine, code = Code.fromFile(file), - baselineLintErrors = lintErrorsPerFile.getOrDefault(fileName, emptyList()), + baselineLintErrors = + lintErrorsPerFile + .getOrDefault( + // Baseline stores the lint violations as relative path to work dir + file.location(true), + emptyList(), + ), ) } - }.parallel({ (fileName, errList) -> report(Paths.get(fileName).relativeRoute, errList, reporter) }) + }.parallel({ (file, errList) -> report(file.location(relative), errList, reporter) }) } private fun lintStdin( @@ -734,6 +737,15 @@ internal fun List.toFilesURIList() = jarFile.toURI().toURL() } +/** + * Wrapper around exitProcess which ensure that a proper log line is written which can be used in unit tests for + * validating the result of the test. + */ +internal fun exitKtLintProcess(status: Int): Nothing { + logger.debug { "Exit ktlint with exit code: $status" } + exitProcess(status) +} + /** * Gets the relative route of the path. Also adjusts the slashes for uniformity between file systems. */ @@ -745,12 +757,3 @@ internal val Path.relativeRoute: String .pathString .replace(File.separatorChar, '/') } - -/** - * Wrapper around exitProcess which ensure that a proper log line is written which can be used in unit tests for - * validating the result of the test. - */ -internal fun exitKtLintProcess(status: Int): Nothing { - logger.debug { "Exit ktlint with exit code: $status" } - exitProcess(status) -} diff --git a/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/api/BaselineCLITest.kt b/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/api/BaselineCLITest.kt index a189ad66cc..4d4f6bce22 100644 --- a/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/api/BaselineCLITest.kt +++ b/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/api/BaselineCLITest.kt @@ -15,9 +15,10 @@ class BaselineCLITest { @TempDir tempDir: Path, ) { + val projectName = "baseline" CommandLineTestRunner(tempDir) .run( - "baseline", + projectName, listOf( "TestBaselineFile.kt.test", "some/path/to/TestBaselineFile2.kt.test", @@ -26,10 +27,10 @@ class BaselineCLITest { SoftAssertions().apply { assertErrorExitCode() assertThat(normalOutput) - .containsLineMatching(Regex("^TestBaselineFile.kt.test:1:24: Unnecessary block.*")) - .containsLineMatching(Regex("^TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) - .containsLineMatching(Regex("^some/path/to/TestBaselineFile2.kt.test:1:25: Unnecessary block.*")) - .containsLineMatching(Regex("^some/path/to/TestBaselineFile2.kt.test:2:1: Unexpected blank line.*")) + .containsLineMatching(Regex(".*/$projectName/TestBaselineFile.kt.test:1:24: Unnecessary block.*")) + .containsLineMatching(Regex(".*/$projectName/TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) + .containsLineMatching(Regex(".*/$projectName/some/path/to/TestBaselineFile2.kt.test:1:25: Unnecessary block.*")) + .containsLineMatching(Regex(".*/$projectName/some/path/to/TestBaselineFile2.kt.test:2:1: Unexpected blank line.*")) }.assertAll() } } @@ -41,11 +42,12 @@ class BaselineCLITest { @TempDir tempDir: Path, ) { + val projectName = "baseline" val baselinePath = "test-baseline.xml" CommandLineTestRunner(tempDir) .run( - "baseline", + projectName, listOf( "--baseline=$baselinePath", "TestBaselineFile.kt.test", @@ -55,10 +57,14 @@ class BaselineCLITest { SoftAssertions().apply { assertNormalExitCode() assertThat(normalOutput) - .doesNotContainLineMatching(Regex("^TestBaselineFile.kt.test:1:24: Unnecessary block.*")) - .doesNotContainLineMatching(Regex("^TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) - .doesNotContainLineMatching(Regex("^some/path/to/TestBaselineFile.kt.test:1:24: Unnecessary block.*")) - .doesNotContainLineMatching(Regex("^some/path/to/TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) + .doesNotContainLineMatching(Regex(".*/$projectName/TestBaselineFile.kt.test:1:24: Unnecessary block.*")) + .doesNotContainLineMatching(Regex(".*/$projectName/TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) + .doesNotContainLineMatching( + Regex(".*/$projectName/some/path/to/TestBaselineFile.kt.test:1:24: Unnecessary block.*"), + ) + .doesNotContainLineMatching( + Regex(".*/$projectName/some/path/to/TestBaselineFile.kt.test:2:1: Unexpected blank line.*"), + ) .containsLineMatching( Regex( ".*Baseline file '$baselinePath' contains 6 reference\\(s\\) to rule ids without a rule set id. For " + @@ -75,11 +81,12 @@ class BaselineCLITest { @TempDir tempDir: Path, ) { + val projectName = "baseline" val baselinePath = "config/test-baseline.xml" CommandLineTestRunner(tempDir) .run( - "baseline", + projectName, listOf( "--baseline=$baselinePath", "TestBaselineFile.kt.test", @@ -89,10 +96,14 @@ class BaselineCLITest { SoftAssertions().apply { assertNormalExitCode() assertThat(normalOutput) - .doesNotContainLineMatching(Regex("^TestBaselineFile.kt.test:1:24: Unnecessary block.*")) - .doesNotContainLineMatching(Regex("^TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) - .doesNotContainLineMatching(Regex("^some/path/to/TestBaselineFile.kt.test:1:24: Unnecessary block.*")) - .doesNotContainLineMatching(Regex("^some/path/to/TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) + .doesNotContainLineMatching(Regex(".*/$projectName/TestBaselineFile.kt.test:1:24: Unnecessary block.*")) + .doesNotContainLineMatching(Regex(".*/$projectName/TestBaselineFile.kt.test:2:1: Unexpected blank line.*")) + .doesNotContainLineMatching( + Regex(".*/$projectName/some/path/to/TestBaselineFile.kt.test:1:24: Unnecessary block.*"), + ) + .doesNotContainLineMatching( + Regex(".*/$projectName/some/path/to/TestBaselineFile.kt.test:2:1: Unexpected blank line.*"), + ) .containsLineMatching( Regex( ".*Baseline file '$baselinePath' contains 6 reference\\(s\\) to rule ids without a rule set id. For " + @@ -145,11 +156,12 @@ class BaselineCLITest { @TempDir tempDir: Path, ) { + val projectName = "baseline" val baselinePath = "test-baseline.xml" CommandLineTestRunner(tempDir) .run( - "baseline", + projectName, listOf( "--baseline=$baselinePath", "TestBaselineExtraErrorFile.kt.test", @@ -159,8 +171,10 @@ class BaselineCLITest { SoftAssertions().apply { assertErrorExitCode() assertThat(normalOutput) - .containsLineMatching(Regex("^TestBaselineExtraErrorFile.kt.test:2:1: Unexpected blank line.*")) - .containsLineMatching(Regex("^some/path/to/TestBaselineExtraErrorFile2.kt.test:2:1: Unexpected blank line.*")) + .containsLineMatching(Regex(".*/$projectName/TestBaselineExtraErrorFile.kt.test:2:1: Unexpected blank line.*")) + .containsLineMatching( + Regex(".*/$projectName/some/path/to/TestBaselineExtraErrorFile2.kt.test:2:1: Unexpected blank line.*"), + ) .containsLineMatching( Regex( ".*Baseline file '$baselinePath' contains 6 reference\\(s\\) to rule ids without a rule set id. For " + From 33a888f4cfc4d9f2ba73b5b94953785f39028222 Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Fri, 5 May 2023 22:48:08 +0200 Subject: [PATCH 32/80] Prevent nullpointer exception (NPE) if or operator at start of line is followed by dot qualified expression (#1994) Closes #1993 --- CHANGELOG.md | 1 + .../ruleset/standard/rules/IndentationRule.kt | 2 +- .../standard/rules/IndentationRuleTest.kt | 26 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cba7fb047b..d6fdb9ef9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). * Print absolute path of file in lint violations when flag "--relative" is not specified in Ktlint CLI ([#1963](https://github.com/pinterest/ktlint/issues/1963)) * Handle parameter `--code-style=android_studio` in Ktlint CLI identical to deprecated parameter `--android` ([#1982](https://github.com/pinterest/ktlint/issues/1982)) * Prevent nullpointer exception (NPE) if class without body is followed by multiple blank lines until end of file `no-consecutive-blank-lines` ([#1987](https://github.com/pinterest/ktlint/issues/1987)) +* Prevent nullpointer exception (NPE) if or operator at start of line is followed by dot qualified expression `indent` ([#1993](https://github.com/pinterest/ktlint/issues/1993)) ### Changed * Separated Baseline functionality out of `ktlint-cli` into separate `ktlint-baseline` module for API consumers diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt index 81142c414f..c802acac00 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt @@ -1091,7 +1091,7 @@ public class IndentationRule : private fun ASTNode?.isElvisOperator() = this != null && elementType == OPERATION_REFERENCE && - firstChildNode.elementType == ELVIS + firstChildNode?.elementType == ELVIS private fun ASTNode.acceptableTrailingSpaces(): String { require(elementType == WHITE_SPACE) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt index f1c49672ef..8ba35fc243 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt @@ -5122,6 +5122,32 @@ internal class IndentationRuleTest { } } + @Test + fun `Issue 1993 - An or operator at start of line followed by a dot qualified expressions should not throw an exception`() { + val code = + """ + val foo = + if (false + || foobar.bar() + ) { + // Do something + } + """.trimIndent() + val formattedCode = + """ + val foo = + if (false || + foobar.bar() + ) { + // Do something + } + """.trimIndent() + indentationRuleAssertThat(code) + .addAdditionalRuleProvider { ChainWrappingRule() } + .hasLintViolationForAdditionalRule(3, 9, "Line must not begin with \"||\"") + .isFormattedAs(formattedCode) + } + private companion object { val INDENT_STYLE_TAB = INDENT_STYLE_PROPERTY to PropertyType.IndentStyleValue.tab From 6e2504586fc3434c9754b76fb932cddb18d82772 Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Fri, 5 May 2023 23:14:28 +0200 Subject: [PATCH 33/80] Allow to 'unset' the `.editorconfig` property `ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than` when using `ktlint_official` code style `function-signature` (#1995) Closes #1977 --- CHANGELOG.md | 1 + docs/rules/configuration-ktlint.md | 7 +- .../api/consumer/KtLintRuleEngineTest.kt | 2 +- .../editorconfig/ec4j/EditorConfigProperty.kt | 36 ++++++ .../core/api/editorconfig/EditorConfigTest.kt | 2 +- .../IndentSizeEditorConfigPropertyTest.kt | 2 +- .../MaxLineLengthEditorConfigPropertyTest.kt | 2 +- .../engine/internal/EditorConfigLoader.kt | 1 + .../engine/internal/EditorConfigProperty.kt | 24 ++-- .../standard/rules/FunctionSignatureRule.kt | 17 ++- .../rules/FunctionSignatureRuleTest.kt | 113 ++++++++++++++++++ 11 files changed, 188 insertions(+), 19 deletions(-) create mode 100644 ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/ec4j/EditorConfigProperty.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index d6fdb9ef9d..b7d5f5fdd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). * Print absolute path of file in lint violations when flag "--relative" is not specified in Ktlint CLI ([#1963](https://github.com/pinterest/ktlint/issues/1963)) * Handle parameter `--code-style=android_studio` in Ktlint CLI identical to deprecated parameter `--android` ([#1982](https://github.com/pinterest/ktlint/issues/1982)) * Prevent nullpointer exception (NPE) if class without body is followed by multiple blank lines until end of file `no-consecutive-blank-lines` ([#1987](https://github.com/pinterest/ktlint/issues/1987)) +* Allow to 'unset' the `.editorconfig` property `ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than` when using `ktlint_official` code style `function-signature` ([#1977](https://github.com/pinterest/ktlint/issues/1977)) * Prevent nullpointer exception (NPE) if or operator at start of line is followed by dot qualified expression `indent` ([#1993](https://github.com/pinterest/ktlint/issues/1993)) ### Changed diff --git a/docs/rules/configuration-ktlint.md b/docs/rules/configuration-ktlint.md index 891d08843a..bd7533867f 100644 --- a/docs/rules/configuration-ktlint.md +++ b/docs/rules/configuration-ktlint.md @@ -59,11 +59,14 @@ This setting only takes effect when rule `final-newline` is enabled. ## Force multiline function signature based on number of parameters -By default, the number of parameters in a function signature is not relevant when rewriting the function signature. Only the maximum line length determines when a function signature should be written on a single line or with multiple lines. Setting `ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than` can be used, to force a multiline function signature in case the function contain at least a number of parameters even in case the function signature would fit on a single line. Use value `-1` (default) to disable this setting. +Setting `ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than` forces a multiline function signature in case the function contains the specified minimum number of parameters even in case the function signature would fit on a single line. Use value `unset` (default) to disable this setting. + +!!! note + By default, the `ktlint_official` code style wraps parameters of functions having at least 2 parameters. For other code styles, this setting is disabled by default. ```ini [*.{kt,kts}] -ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than= -1 +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than=unset ``` This setting only takes effect when rule `function-signature` is enabled. diff --git a/ktlint-api-consumer/src/test/kotlin/com/pinterest/ktlint/api/consumer/KtLintRuleEngineTest.kt b/ktlint-api-consumer/src/test/kotlin/com/pinterest/ktlint/api/consumer/KtLintRuleEngineTest.kt index fda8dd30a8..e97970362b 100644 --- a/ktlint-api-consumer/src/test/kotlin/com/pinterest/ktlint/api/consumer/KtLintRuleEngineTest.kt +++ b/ktlint-api-consumer/src/test/kotlin/com/pinterest/ktlint/api/consumer/KtLintRuleEngineTest.kt @@ -12,7 +12,7 @@ import com.pinterest.ktlint.rule.engine.core.api.RuleProvider import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EXPERIMENTAL_RULES_EXECUTION_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.editorconfig.RuleExecution import com.pinterest.ktlint.rule.engine.core.api.editorconfig.createRuleExecutionEditorConfigProperty -import com.pinterest.ktlint.rule.engine.internal.toPropertyBuilderWithValue +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyBuilderWithValue import com.pinterest.ktlint.ruleset.standard.rules.FilenameRule import com.pinterest.ktlint.ruleset.standard.rules.IndentationRule import com.pinterest.ktlint.test.KtlintTestFileSystem diff --git a/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/ec4j/EditorConfigProperty.kt b/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/ec4j/EditorConfigProperty.kt new file mode 100644 index 0000000000..c918465173 --- /dev/null +++ b/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/ec4j/EditorConfigProperty.kt @@ -0,0 +1,36 @@ +package com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j + +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty +import org.ec4j.core.model.Property +import org.ec4j.core.model.PropertyType + +/** + * Creates an [org.ec4j.core.model.Property.Builder] for given [value]. + */ +public fun EditorConfigProperty.toPropertyBuilderWithValue(value: String): Property.Builder = + Property + .builder() + .type(type) + .name(name) + .value(value) + +/** + * Creates an [org.ec4j.core.model.Property] for given [value]. + */ +public fun EditorConfigProperty.toPropertyWithValue(value: String): Property = toPropertyBuilderWithValue(value).build() + +/** + * Creates an [org.ec4j.core.model.Property.Builder] for given [value]. + */ +public fun EditorConfigProperty.toPropertyBuilderWithValue(value: PropertyType.PropertyValue<*>): Property.Builder = + Property + .builder() + .type(type) + .name(name) + .value(value) + +/** + * Creates an [org.ec4j.core.model.Property] for given [value]. + */ +public fun EditorConfigProperty.toPropertyWithValue(value: PropertyType.PropertyValue<*>): Property = + toPropertyBuilderWithValue(value).build() diff --git a/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfigTest.kt b/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfigTest.kt index 3b3f77ccc6..af32e74763 100644 --- a/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfigTest.kt +++ b/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfigTest.kt @@ -1,7 +1,7 @@ package com.pinterest.ktlint.rule.engine.core.api.editorconfig import com.pinterest.ktlint.rule.engine.api.EditorConfigDefaults -import com.pinterest.ktlint.rule.engine.internal.toPropertyWithValue +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyWithValue import com.pinterest.ktlint.test.KtlintTestFileSystem import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatNoException diff --git a/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/IndentSizeEditorConfigPropertyTest.kt b/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/IndentSizeEditorConfigPropertyTest.kt index 7e7c533e97..a393c46efd 100644 --- a/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/IndentSizeEditorConfigPropertyTest.kt +++ b/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/IndentSizeEditorConfigPropertyTest.kt @@ -1,6 +1,6 @@ package com.pinterest.ktlint.rule.engine.core.api.editorconfig -import com.pinterest.ktlint.rule.engine.internal.toPropertyWithValue +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyWithValue import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource diff --git a/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/MaxLineLengthEditorConfigPropertyTest.kt b/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/MaxLineLengthEditorConfigPropertyTest.kt index e4a56d3925..a190c70dd4 100644 --- a/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/MaxLineLengthEditorConfigPropertyTest.kt +++ b/ktlint-rule-engine-core/src/test/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/MaxLineLengthEditorConfigPropertyTest.kt @@ -1,6 +1,6 @@ package com.pinterest.ktlint.rule.engine.core.api.editorconfig -import com.pinterest.ktlint.rule.engine.internal.toPropertyWithValue +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyWithValue import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Nested import org.junit.jupiter.params.ParameterizedTest diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigLoader.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigLoader.kt index b79ba04e1e..7957998ee8 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigLoader.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigLoader.kt @@ -13,6 +13,7 @@ import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EXPERIMENTAL_RULES import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyWithValue import com.pinterest.ktlint.rule.engine.internal.FormatterTags.Companion.FORMATTER_TAGS_ENABLED_PROPERTY import com.pinterest.ktlint.rule.engine.internal.FormatterTags.Companion.FORMATTER_TAG_OFF_ENABLED_PROPERTY import com.pinterest.ktlint.rule.engine.internal.FormatterTags.Companion.FORMATTER_TAG_ON_ENABLED_PROPERTY diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigProperty.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigProperty.kt index 6eac4c9ae3..648fbea38b 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigProperty.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/EditorConfigProperty.kt @@ -3,34 +3,34 @@ package com.pinterest.ktlint.rule.engine.internal import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty import org.ec4j.core.model.Property import org.ec4j.core.model.PropertyType +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyBuilderWithValue as ec4jToPropertyBuilderWithPropertyValue +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyBuilderWithValue as ec4jToPropertyBuilderWithStringValue +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyWithValue as ec4jToPropertyWithPropertyValue +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyWithValue as ec4jToPropertyWithStringValue /** * Creates an [org.ec4j.core.model.Property.Builder] for given [value]. */ +@Deprecated("Marked for removal in ktlint 0.50. Use function exposed by ktlint-rule-engine-core instead") public fun EditorConfigProperty.toPropertyBuilderWithValue(value: String): Property.Builder = - Property - .builder() - .type(type) - .name(name) - .value(value) + ec4jToPropertyBuilderWithStringValue(value) /** * Creates an [org.ec4j.core.model.Property] for given [value]. */ -public fun EditorConfigProperty.toPropertyWithValue(value: String): Property = toPropertyBuilderWithValue(value).build() +@Deprecated("Marked for removal in ktlint 0.50. Use function exposed by ktlint-rule-engine-core instead") +public fun EditorConfigProperty.toPropertyWithValue(value: String): Property = ec4jToPropertyWithStringValue(value) /** * Creates an [org.ec4j.core.model.Property.Builder] for given [value]. */ +@Deprecated("Marked for removal in ktlint 0.50. Use function exposed by ktlint-rule-engine-core instead") public fun EditorConfigProperty.toPropertyBuilderWithValue(value: PropertyType.PropertyValue<*>): Property.Builder = - Property - .builder() - .type(type) - .name(name) - .value(value) + ec4jToPropertyBuilderWithPropertyValue(value) /** * Creates an [org.ec4j.core.model.Property] for given [value]. */ +@Deprecated("Marked for removal in ktlint 0.50. Use function exposed by ktlint-rule-engine-core instead") public fun EditorConfigProperty.toPropertyWithValue(value: PropertyType.PropertyValue<*>): Property = - toPropertyBuilderWithValue(value).build() + ec4jToPropertyWithPropertyValue(value) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRule.kt index 35af2368b5..28a6ebc722 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRule.kt @@ -717,6 +717,7 @@ public class FunctionSignatureRule : ?.prevCodeLeaf() public companion object { + private const val FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY_UNSET = Int.MAX_VALUE public val FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY: EditorConfigProperty = EditorConfigProperty( type = @@ -728,8 +729,22 @@ public class FunctionSignatureRule : PropertyType.PropertyValueParser.POSITIVE_INT_VALUE_PARSER, setOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "unset"), ), - defaultValue = Int.MAX_VALUE, + defaultValue = FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY_UNSET, ktlintOfficialCodeStyleDefaultValue = 2, + propertyMapper = { property, _ -> + if (property?.isUnset == true) { + FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY_UNSET + } else { + property?.getValueAs() + } + }, + propertyWriter = { property -> + if (property == FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY_UNSET) { + "unset" + } else { + property.toString() + } + }, ) public val FUNCTION_BODY_EXPRESSION_WRAPPING_PROPERTY: EditorConfigProperty = diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRuleTest.kt index 26c5a3703c..661faffdac 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionSignatureRuleTest.kt @@ -3,16 +3,20 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue.ktlint_official +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.ec4j.toPropertyWithValue import com.pinterest.ktlint.ruleset.standard.rules.FunctionSignatureRule.Companion.FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY import com.pinterest.ktlint.ruleset.standard.rules.FunctionSignatureRule.Companion.FUNCTION_BODY_EXPRESSION_WRAPPING_PROPERTY import com.pinterest.ktlint.test.KtLintAssertThat.Companion.EOL_CHAR import com.pinterest.ktlint.test.KtLintAssertThat.Companion.MAX_LINE_LENGTH_MARKER import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule import com.pinterest.ktlint.test.LintViolation +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import org.junit.jupiter.params.provider.EnumSource class FunctionSignatureRuleTest { @@ -1131,6 +1135,115 @@ class FunctionSignatureRuleTest { } } + @Nested + inner class `Property ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than` { + val propertyMapper = FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY.propertyMapper!! + + @ParameterizedTest(name = "Code style: {0}") + @EnumSource(CodeStyleValue::class) + fun `Given a null property then the property mapper returns null`(codeStyleValue: CodeStyleValue) { + val actual = propertyMapper(null, codeStyleValue) + + assertThat(actual).isNull() + } + + @ParameterizedTest(name = "Code style: {0}") + @EnumSource(CodeStyleValue::class) + fun `Given a property which is unset then the property mapper returns max integer which is set as the default value`( + codeStyleValue: CodeStyleValue, + ) { + val property = FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY.toPropertyWithValue("unset") + + val actual = propertyMapper(property, codeStyleValue) + + assertThat(actual).isEqualTo(Int.MAX_VALUE) + } + + @ParameterizedTest(name = "Code style: {0}") + @EnumSource(CodeStyleValue::class) + fun `Given a valid string value then the property mapper returns the integer value`(codeStyleValue: CodeStyleValue) { + val someValue = 123 + val property = FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY.toPropertyWithValue(someValue.toString()) + + val actual = propertyMapper(property, codeStyleValue) + + assertThat(actual).isEqualTo(someValue) + } + + @ParameterizedTest(name = "Code style: {0}") + @EnumSource(CodeStyleValue::class) + fun `Given a negative value then the property mapper throws and exception`(codeStyleValue: CodeStyleValue) { + val someNegativeValue = "-1" + val property = FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY.toPropertyWithValue(someNegativeValue) + + Assertions.assertThatExceptionOfType(RuntimeException::class.java) + .isThrownBy { propertyMapper(property, codeStyleValue) } + .withMessage( + "Property 'ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than' expects a " + + "positive integer; found '$someNegativeValue'", + ) + } + + @ParameterizedTest(name = "Code style: {0}") + @EnumSource(CodeStyleValue::class) + fun `Given a value bigger than max integer then the property mapper throws and exception`(codeStyleValue: CodeStyleValue) { + val someValueBiggerThanMaxInt = (1L + Int.MAX_VALUE).toString() + val property = + FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY.toPropertyWithValue(someValueBiggerThanMaxInt) + + Assertions.assertThatExceptionOfType(RuntimeException::class.java) + .isThrownBy { propertyMapper(property, codeStyleValue) } + .withMessage( + "Property 'ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than' expects an " + + "integer. The parsed '$someValueBiggerThanMaxInt' is not an integer.", + ) + } + + @ParameterizedTest(name = "Code style: {0}") + @EnumSource(CodeStyleValue::class) + fun `Given a invalid string value then the property mapper returns the integer value`(codeStyleValue: CodeStyleValue) { + val property = + FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY.toPropertyWithValue("some-invalid-value") + + Assertions.assertThatExceptionOfType(RuntimeException::class.java) + .isThrownBy { propertyMapper(property, codeStyleValue) } + .withMessage( + "Property 'ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than' expects an " + + "integer. The parsed 'some-invalid-value' is not an integer.", + ) + } + + @ParameterizedTest(name = "Input value: {0}, output value: {1}") + @CsvSource( + value = [ + "1, 1", + "${Int.MAX_VALUE}, unset", + ], + ) + fun `Given a property with an integer value than write that property`( + inputValue: Int, + expectedOutputValue: String, + ) { + val actual = FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY.propertyWriter(inputValue) + + assertThat(actual).isEqualTo(expectedOutputValue) + } + } + + @Test + fun `Given the ktlint_official code style then avoid wrapping of parameters by overriding property ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than`() { + val code = + """ + fun f(a: Any, b: Any, c: Any): String { + // body + } + """.trimIndent() + functionSignatureWrappingRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .withEditorConfigOverride(FORCE_MULTILINE_WHEN_PARAMETER_COUNT_GREATER_OR_EQUAL_THAN_PROPERTY to "unset") + .hasNoLintViolations() + } + private companion object { const val UNEXPECTED_SPACES = " " const val NO_SPACE = "" From fc16aa0d4fe55956a4d6703067a72d3ae4c8a11a Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Sun, 7 May 2023 16:06:38 +0200 Subject: [PATCH 34/80] Restrict indentation of closing quotes to `ktlint_official` code style (#1996) * Restrict indentation of closing quotes to `ktlint_official` code style to keep formatting of other code styles consistent with `0.48.x` and before Closes #1971 --- CHANGELOG.md | 1 + .../ruleset/standard/rules/IndentationRule.kt | 12 +- .../standard/rules/IndentationRuleTest.kt | 165 +++++++++++++----- 3 files changed, 131 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7d5f5fdd3..ab67c249ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). * Prevent nullpointer exception (NPE) if class without body is followed by multiple blank lines until end of file `no-consecutive-blank-lines` ([#1987](https://github.com/pinterest/ktlint/issues/1987)) * Allow to 'unset' the `.editorconfig` property `ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than` when using `ktlint_official` code style `function-signature` ([#1977](https://github.com/pinterest/ktlint/issues/1977)) * Prevent nullpointer exception (NPE) if or operator at start of line is followed by dot qualified expression `indent` ([#1993](https://github.com/pinterest/ktlint/issues/1993)) +* Restrict indentation of closing quotes to `ktlint_official` code style to keep formatting of other code styles consistent with `0.48.x` and before `indent` ([#1971](https://github.com/pinterest/ktlint/issues/1971)) ### Changed * Separated Baseline functionality out of `ktlint-cli` into separate `ktlint-baseline` module for API consumers diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt index c802acac00..65465d1e45 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt @@ -89,6 +89,7 @@ import com.pinterest.ktlint.rule.engine.core.api.Rule.VisitorModifier.RunAfterRu import com.pinterest.ktlint.rule.engine.core.api.RuleId import com.pinterest.ktlint.rule.engine.core.api.children import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue.ktlint_official import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY @@ -1083,7 +1084,7 @@ public class IndentationRule : emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, ) { if (!this::stringTemplateIndenter.isInitialized) { - stringTemplateIndenter = StringTemplateIndenter(indentConfig) + stringTemplateIndenter = StringTemplateIndenter(codeStyle, indentConfig) } stringTemplateIndenter.visitClosingQuotes(currentIndent(), node.treeParent, autoCorrect, emit) } @@ -1215,7 +1216,10 @@ private fun String.textWithEscapedTabAndNewline(): String { ).plus(suffix) } -private class StringTemplateIndenter(private val indentConfig: IndentConfig) { +private class StringTemplateIndenter( + private val codeStyle: CodeStyleValue, + private val indentConfig: IndentConfig, +) { fun visitClosingQuotes( expectedIndent: String, node: ASTNode, @@ -1243,7 +1247,7 @@ private class StringTemplateIndenter(private val indentConfig: IndentConfig) { val prefixLength = node.getCommonPrefixLength() val prevLeaf = node.prevLeaf() val correctedExpectedIndent = - if (node.isRawStringLiteralReturnInFunctionBodyBlock()) { + if (codeStyle == ktlint_official && node.isRawStringLiteralReturnInFunctionBodyBlock()) { // Allow: // fun foo(): String { // return """ @@ -1253,7 +1257,7 @@ private class StringTemplateIndenter(private val indentConfig: IndentConfig) { node .indent(false) .plus(indentConfig.indent) - } else if (node.isRawStringLiteralFunctionBodyExpression()) { + } else if (codeStyle == ktlint_official && node.isRawStringLiteralFunctionBodyExpression()) { // Allow: // fun foo( // bar: String diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt index 8ba35fc243..059e278f01 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt @@ -1,6 +1,7 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue.intellij_idea import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue.ktlint_official import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY @@ -5055,32 +5056,6 @@ internal class IndentationRuleTest { @Nested inner class `Given a function with raw string literal as result` { - @Test - fun `As body expression on same line as equals and preceded by space`() { - val code = - """ - private fun foo( - bar: String, - ) = $MULTILINE_STRING_QUOTE - bar - $MULTILINE_STRING_QUOTE.trimIndent() - """.trimIndent() - indentationRuleAssertThat(code).hasNoLintViolations() - } - - @Test - fun `As body expression on same line as equals but not preceded by space`() { - val code = - """ - private fun foo( - bar: String, - ) =$MULTILINE_STRING_QUOTE - bar - $MULTILINE_STRING_QUOTE.trimIndent() - """.trimIndent() - indentationRuleAssertThat(code).hasNoLintViolations() - } - @Test fun `As body expression on next line`() { val code = @@ -5093,32 +5068,136 @@ internal class IndentationRuleTest { indentationRuleAssertThat(code).hasNoLintViolations() } - @Test - fun `As block body`() { - val code = - """ - private fun foo( bar: String): String { - return $MULTILINE_STRING_QUOTE + @Nested + inner class `Given non-ktlint_official code style` { + private val nonKtlintOfficialCodeStyle = CodeStyleValue.android_studio + + @Test + fun `As body expression on same line as equals and preceded by space`() { + val code = + """ + private fun foo( + bar: String, + ) = $MULTILINE_STRING_QUOTE + bar + $MULTILINE_STRING_QUOTE.trimIndent() + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to nonKtlintOfficialCodeStyle) + .hasNoLintViolations() + } + + @Test + fun `As body expression on same line as equals but not preceded by space`() { + val code = + """ + private fun foo( + bar: String, + ) =$MULTILINE_STRING_QUOTE bar + $MULTILINE_STRING_QUOTE.trimIndent() + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to nonKtlintOfficialCodeStyle) + .hasNoLintViolations() + } + + @Test + fun `As block body`() { + val code = + """ + private fun foo( bar: String): String { + return $MULTILINE_STRING_QUOTE + bar $MULTILINE_STRING_QUOTE.trimIndent() - } - """.trimIndent() - indentationRuleAssertThat(code).hasNoLintViolations() + } + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to nonKtlintOfficialCodeStyle) + .hasNoLintViolations() + } + + @Test + fun `As body expression of function wrapped in class`() { + val code = + """ + class Bar { + private fun foo( + bar: String, + ) = $MULTILINE_STRING_QUOTE + bar + $MULTILINE_STRING_QUOTE.trimIndent() + } + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to nonKtlintOfficialCodeStyle) + .hasNoLintViolations() + } } - @Test - fun `As body expression of function wrapped in class`() { - val code = - """ - class Bar { + @Nested + inner class `Given ktlint_official code style` { + @Test + fun `As body expression on same line as equals and preceded by space`() { + val code = + """ private fun foo( bar: String, ) = $MULTILINE_STRING_QUOTE bar $MULTILINE_STRING_QUOTE.trimIndent() - } - """.trimIndent() - indentationRuleAssertThat(code).hasNoLintViolations() + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasNoLintViolations() + } + + @Test + fun `As body expression on same line as equals but not preceded by space`() { + val code = + """ + private fun foo( + bar: String, + ) =$MULTILINE_STRING_QUOTE + bar + $MULTILINE_STRING_QUOTE.trimIndent() + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasNoLintViolations() + } + + @Test + fun `As block body`() { + val code = + """ + private fun foo( bar: String): String { + return $MULTILINE_STRING_QUOTE + bar + $MULTILINE_STRING_QUOTE.trimIndent() + } + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasNoLintViolations() + } + + @Test + fun `As body expression of function wrapped in class`() { + val code = + """ + class Bar { + private fun foo( + bar: String, + ) = $MULTILINE_STRING_QUOTE + bar + $MULTILINE_STRING_QUOTE.trimIndent() + } + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasNoLintViolations() + } } } From 7ba9ed0ea451c7fc326c86d66f7a258e66c14dbe Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Sun, 7 May 2023 16:42:11 +0200 Subject: [PATCH 35/80] Fix indentation of multiline parameter list in function literal (#1997) Closes #1976 --- CHANGELOG.md | 1 + .../ruleset/standard/rules/IndentationRule.kt | 28 +++++++++----- .../rules/ParameterListWrappingRule.kt | 26 ++++++++++++- .../standard/rules/IndentationRuleTest.kt | 9 +++-- .../rules/ParameterListWrappingRuleTest.kt | 37 ++++++++++++++----- 5 files changed, 76 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab67c249ea..0657381500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). * Prevent nullpointer exception (NPE) if class without body is followed by multiple blank lines until end of file `no-consecutive-blank-lines` ([#1987](https://github.com/pinterest/ktlint/issues/1987)) * Allow to 'unset' the `.editorconfig` property `ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than` when using `ktlint_official` code style `function-signature` ([#1977](https://github.com/pinterest/ktlint/issues/1977)) * Prevent nullpointer exception (NPE) if or operator at start of line is followed by dot qualified expression `indent` ([#1993](https://github.com/pinterest/ktlint/issues/1993)) +* Fix indentation of multiline parameter list in function literal `indent` ([#1976](https://github.com/pinterest/ktlint/issues/1976)) * Restrict indentation of closing quotes to `ktlint_official` code style to keep formatting of other code styles consistent with `0.48.x` and before `indent` ([#1971](https://github.com/pinterest/ktlint/issues/1971)) ### Changed diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt index 65465d1e45..39ae35af94 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt @@ -53,7 +53,6 @@ import com.pinterest.ktlint.rule.engine.core.api.ElementType.PROPERTY import com.pinterest.ktlint.rule.engine.core.api.ElementType.PROPERTY_ACCESSOR import com.pinterest.ktlint.rule.engine.core.api.ElementType.RBRACE import com.pinterest.ktlint.rule.engine.core.api.ElementType.RBRACKET -import com.pinterest.ktlint.rule.engine.core.api.ElementType.REFERENCE_EXPRESSION import com.pinterest.ktlint.rule.engine.core.api.ElementType.REGULAR_STRING_PART import com.pinterest.ktlint.rule.engine.core.api.ElementType.RETURN_KEYWORD import com.pinterest.ktlint.rule.engine.core.api.ElementType.RPAR @@ -96,6 +95,7 @@ import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPER import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.firstChildLeafOrSelf import com.pinterest.ktlint.rule.engine.core.api.indent +import com.pinterest.ktlint.rule.engine.core.api.isPartOf import com.pinterest.ktlint.rule.engine.core.api.isPartOfComment import com.pinterest.ktlint.rule.engine.core.api.isRoot import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace @@ -452,7 +452,7 @@ public class IndentationRule : } private fun ASTNode.calculateIndentOfFunctionLiteralParameters() = - if (codeStyle == ktlint_official || isFirstParameterOfFunctionLiteralPrecededByNewLine()) { + if (isFirstParameterOfFunctionLiteralPrecededByNewLine()) { // val fieldExample = // LongNameClass { // paramA, @@ -469,13 +469,23 @@ public class IndentationRule : // paramC -> // ClassB(paramA, paramB, paramC) // } - parent(CALL_EXPRESSION) - ?.let { callExpression -> - val textBeforeFirstParameter = - callExpression.findChildByType(REFERENCE_EXPRESSION)?.text + - " { " - " ".repeat(textBeforeFirstParameter.length) - } + // val fieldExample = + // someFunction( + // 1, + // 2, + // ) { paramA, + // paramB, + // paramC -> + // ClassB(paramA, paramB, paramC) + // } + this + .takeIf { it.isPartOf(CALL_EXPRESSION) } + ?.treeParent + ?.leaves(false) + ?.takeWhile { !it.isWhiteSpaceWithNewline() } + ?.sumOf { it.textLength } + ?.plus(2) // need to add spaces to compensate for "{ " + ?.let { length -> " ".repeat(length) } ?: indentConfig.indent.repeat(2) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRule.kt index 840d0c48c1..3f0926b3a1 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRule.kt @@ -19,8 +19,11 @@ import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.editorconfig.MAX_LINE_LENGTH_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.firstChildLeafOrSelf import com.pinterest.ktlint.rule.engine.core.api.indent import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline +import com.pinterest.ktlint.rule.engine.core.api.parent +import com.pinterest.ktlint.rule.engine.core.api.prevCodeLeaf import com.pinterest.ktlint.rule.engine.core.api.prevLeaf import com.pinterest.ktlint.rule.engine.core.api.prevSibling import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceAfterMe @@ -33,6 +36,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.psi.KtTypeArgumentList import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType +import org.jetbrains.kotlin.psi.psiUtil.leaves public class ParameterListWrappingRule : StandardRule( @@ -120,7 +124,9 @@ public class ParameterListWrappingRule : private fun ASTNode.needToWrapParameterList() = when { hasNoParameters() -> false - isPartOfFunctionLiteralInNonKtlintOfficialCodeStyle() -> false + codeStyle != ktlint_official && isPartOfFunctionLiteralInNonKtlintOfficialCodeStyle() -> false + codeStyle == ktlint_official && isPartOfFunctionLiteralStartingOnSameLineAsClosingParenthesisOfPrecedingReferenceExpression() -> + false isFunctionTypeWrappedInNullableType() -> false textContains('\n') -> true codeStyle == ktlint_official && containsAnnotatedParameter() -> true @@ -135,7 +141,23 @@ public class ParameterListWrappingRule : private fun ASTNode.isPartOfFunctionLiteralInNonKtlintOfficialCodeStyle(): Boolean { require(elementType == VALUE_PARAMETER_LIST) - return codeStyle != ktlint_official && treeParent?.elementType == FUNCTION_LITERAL + return treeParent?.elementType == FUNCTION_LITERAL + } + + private fun ASTNode.isPartOfFunctionLiteralStartingOnSameLineAsClosingParenthesisOfPrecedingReferenceExpression(): Boolean { + require(elementType == VALUE_PARAMETER_LIST) + return firstChildLeafOrSelf() + .let { startOfFunctionLiteral -> + treeParent + ?.takeIf { it.elementType == FUNCTION_LITERAL } + ?.prevCodeLeaf() + ?.takeIf { it.treeParent.elementType == ElementType.VALUE_ARGUMENT_LIST } + ?.takeIf { it.treeParent.treeParent.elementType == ElementType.CALL_EXPRESSION } + ?.leaves() + ?.takeWhile { it != startOfFunctionLiteral } + ?.none { it.isWhiteSpaceWithNewline() } + ?: false + } } private fun ASTNode.isFunctionTypeWrappedInNullableType(): Boolean { diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt index 059e278f01..5e816c0b2f 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt @@ -4819,10 +4819,11 @@ internal class IndentationRuleTest { .addAdditionalRuleProvider { ParameterListWrappingRule() } .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) .hasLintViolationForAdditionalRule(2, 20, "Parameter should start on a newline") - .hasLintViolations( - LintViolation(3, 1, "Unexpected indentation (19) (should be 12)"), - LintViolation(4, 1, "Unexpected indentation (19) (should be 12)"), - ).isFormattedAs(formattedCode) +// .hasLintViolations( +// LintViolation(3, 1, "Unexpected indentation (19) (should be 12)"), +// LintViolation(4, 1, "Unexpected indentation (19) (should be 12)"), +// ) + .isFormattedAs(formattedCode) } @Test diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRuleTest.kt index 5cc3aba446..d96a3ee426 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRuleTest.kt @@ -179,7 +179,7 @@ class ParameterListWrappingRuleTest { } @Nested - inner class `Given a function literal having a multiline parameter list` { + inner class `Given a function literal having a multiline parameter list and the first parameter starts on same line as LBRACE` { private val code = """ val fieldExample = @@ -204,10 +204,9 @@ class ParameterListWrappingRuleTest { """.trimIndent() parameterListWrappingRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) - .hasLintViolationsForAdditionalRule( - LintViolation(3, 1, "Unexpected indentation (20) (should be 12)"), - LintViolation(4, 1, "Unexpected indentation (20) (should be 12)"), - ).isFormattedAs(formattedCode) + // Indent violations will not be reported until after the wrapping of the first parameter is completed and as of that will + // not be found during linting + .isFormattedAs(formattedCode) } @ParameterizedTest(name = "Code style = {0}") @@ -216,17 +215,35 @@ class ParameterListWrappingRuleTest { mode = EnumSource.Mode.EXCLUDE, names = ["ktlint_official"], ) - fun `Given another than ktlint_official code style then do not reformat`(codeStyleValue: CodeStyleValue) { + fun `Given another code style than ktlint_official then do not reformat`(codeStyleValue: CodeStyleValue) { parameterListWrappingRuleAssertThat(code) .withEditorConfigOverride(CODE_STYLE_PROPERTY to codeStyleValue) .hasNoLintViolations() -// .hasLintViolationsForAdditionalRule( -// LintViolation(3, 1, "Unexpected indentation (20) (should be 12)"), -// LintViolation(4, 1, "Unexpected indentation (20) (should be 12)"), -// ) } } + @ParameterizedTest(name = "Code style = {0}") + @EnumSource(value = CodeStyleValue::class) + fun `Given a multiline reference expression with trailing lambda having a multiline parameter list and the first parameter starts on same line as LBRACE`( + codeStyleValue: CodeStyleValue, + ) { + val code = + """ + val foo = + bar( + Any(), + Any() + ) { a, + b + -> + foobar() + } + """.trimIndent() + parameterListWrappingRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to codeStyleValue) + .hasNoLintViolations() + } + @Test fun `Given a function with annotated parameters then start each parameter on a separate line but preserve spacing between annotation and parameter name`() { val code = From 6500181e1652383d1eb29d2feb84aad9f2897921 Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Sun, 7 May 2023 20:18:48 +0200 Subject: [PATCH 36/80] Clean up logging dependencies (#1999) Closes #1998 --- CHANGELOG.md | 1 + .../internal/ThreadSafeEditorConfigCacheTest.kt | 15 --------------- ktlint-ruleset-standard/build.gradle.kts | 1 - ktlint-test/build.gradle.kts | 1 - 4 files changed, 1 insertion(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0657381500..45e925bbca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). * Prevent nullpointer exception (NPE) if or operator at start of line is followed by dot qualified expression `indent` ([#1993](https://github.com/pinterest/ktlint/issues/1993)) * Fix indentation of multiline parameter list in function literal `indent` ([#1976](https://github.com/pinterest/ktlint/issues/1976)) * Restrict indentation of closing quotes to `ktlint_official` code style to keep formatting of other code styles consistent with `0.48.x` and before `indent` ([#1971](https://github.com/pinterest/ktlint/issues/1971)) +* Clean-up unwanted logging dependencies ([#1998](https://github.com/pinterest/ktlint/issues/1998)) ### Changed * Separated Baseline functionality out of `ktlint-cli` into separate `ktlint-baseline` module for API consumers diff --git a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/ThreadSafeEditorConfigCacheTest.kt b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/ThreadSafeEditorConfigCacheTest.kt index 90d0708b9a..ca45cac1c6 100644 --- a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/ThreadSafeEditorConfigCacheTest.kt +++ b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/ThreadSafeEditorConfigCacheTest.kt @@ -1,10 +1,5 @@ package com.pinterest.ktlint.rule.engine.internal -import ch.qos.logback.classic.Level -import ch.qos.logback.classic.Logger -import com.pinterest.ktlint.logger.api.initKtLintKLogger -import com.pinterest.ktlint.logger.api.setDefaultLoggerModifier -import mu.KotlinLogging import org.assertj.core.api.Assertions.assertThat import org.ec4j.core.EditorConfigLoader import org.ec4j.core.Resource @@ -17,16 +12,6 @@ import java.nio.charset.StandardCharsets import java.nio.file.Paths class ThreadSafeEditorConfigCacheTest { - init { - // Overwrite default logging with TRACE logging by initializing *and* printing first log statement before - // loading any other classes. - KotlinLogging - .logger {} - .setDefaultLoggerModifier { logger -> (logger.underlyingLogger as Logger).level = Level.TRACE } - .initKtLintKLogger() - .trace { "Enable trace logging for unit test" } - } - @Test fun `Given a file which is requested multiple times then it is read only once and then stored into and retrieved from the cache`() { val threadSafeEditorConfigCache = ThreadSafeEditorConfigCache() diff --git a/ktlint-ruleset-standard/build.gradle.kts b/ktlint-ruleset-standard/build.gradle.kts index 155380a27a..e97bbf2a54 100644 --- a/ktlint-ruleset-standard/build.gradle.kts +++ b/ktlint-ruleset-standard/build.gradle.kts @@ -4,7 +4,6 @@ plugins { dependencies { implementation(projects.ktlintLogger) - implementation(libs.logging) api(projects.ktlintCliRulesetCore) api(projects.ktlintRuleEngineCore) diff --git a/ktlint-test/build.gradle.kts b/ktlint-test/build.gradle.kts index 8e492dcdb1..6262c050d4 100644 --- a/ktlint-test/build.gradle.kts +++ b/ktlint-test/build.gradle.kts @@ -8,7 +8,6 @@ dependencies { implementation(projects.ktlintCliRulesetCore) api(libs.assertj) api(libs.junit5) - api(libs.logback) api(libs.janino) api(libs.jimfs) } From bc39250a0ed84f72b091b52e2b4d85737f93ce97 Mon Sep 17 00:00:00 2001 From: Avinash M Date: Tue, 9 May 2023 15:35:12 +0530 Subject: [PATCH 37/80] Fix interchanged sample code; rule no-empty-first-line-in-method-block (#2003) --- docs/rules/standard.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/standard.md b/docs/rules/standard.md index 12ff505919..e64adc0011 100644 --- a/docs/rules/standard.md +++ b/docs/rules/standard.md @@ -450,7 +450,6 @@ Rule id: `no-empty-class-body` ```kotlin fun bar() { - val a = 2 } ``` @@ -458,6 +457,7 @@ Rule id: `no-empty-class-body` ```kotlin fun bar() { + val a = 2 } ``` From 9256b8eeeeb197a1440d4387067f410d670d0ad3 Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Tue, 9 May 2023 12:41:32 +0200 Subject: [PATCH 38/80] Fix directory traversal for patterns outside workdirectory (#2004) Fix directory traversal for patterns referring to paths outside of current working directory or any of it child directories. Note that on Windows the (relative) patterns may not refer to a path outside of the current working directory. Closes #2002 --- CHANGELOG.md | 1 + .../ktlint/cli/internal/FileUtils.kt | 43 +++++++----- .../ktlint/cli/internal/FileUtilsTest.kt | 65 +++++++++++++++++++ 3 files changed, 94 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e925bbca..7665bcc167 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). * Fix indentation of multiline parameter list in function literal `indent` ([#1976](https://github.com/pinterest/ktlint/issues/1976)) * Restrict indentation of closing quotes to `ktlint_official` code style to keep formatting of other code styles consistent with `0.48.x` and before `indent` ([#1971](https://github.com/pinterest/ktlint/issues/1971)) * Clean-up unwanted logging dependencies ([#1998](https://github.com/pinterest/ktlint/issues/1998)) +* Fix directory traversal for patterns referring to paths outside of current working directory or any of it child directories ([#2002](https://github.com/pinterest/ktlint/issues/2002)) ### Changed * Separated Baseline functionality out of `ktlint-cli` into separate `ktlint-baseline` module for API consumers diff --git a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt index 81d795450d..62506a50fb 100644 --- a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt +++ b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt @@ -14,7 +14,6 @@ import java.nio.file.SimpleFileVisitor import java.nio.file.attribute.BasicFileAttributes import java.util.Deque import java.util.LinkedList -import kotlin.io.path.Path import kotlin.io.path.absolutePathString import kotlin.io.path.isDirectory import kotlin.io.path.pathString @@ -66,7 +65,7 @@ internal fun FileSystem.fileSequence( .filter { it.startsWith(NEGATION_PREFIX) } .map { getPathMatcher(it.removePrefix(NEGATION_PREFIX)) } - val pathMatchers = + val includeGlobs = globs .filterNot { it.startsWith(NEGATION_PREFIX) } .let { includeMatchers -> @@ -81,23 +80,27 @@ internal fun FileSystem.fileSequence( } else { includeMatchers } - }.map { getPathMatcher(it) } - - LOGGER.debug { - """ - Start walkFileTree for rootDir: '$rootDir' - include: - ${pathMatchers.map { - " - $it" - }} - exclude: - ${negatedPathMatchers.map { " - $it" }} - """.trimIndent() + } + var commonRootDir = rootDir + patterns.forEach { pattern -> + try { + val patternDir = + rootDir + .resolve(pattern) + .normalize() + commonRootDir = commonRootDir.findCommonParentDir(patternDir) + } catch (e: InvalidPathException) { + // Windows throws an exception when you pass a glob to Path#resolve. + } } + + val pathMatchers = includeGlobs.map { getPathMatcher(it) } + + LOGGER.debug { "Start walkFileTree from directory: '$commonRootDir'" } val duration = measureTimeMillis { Files.walkFileTree( - rootDir, + commonRootDir, object : SimpleFileVisitor() { override fun visitFile( filePath: Path, @@ -148,6 +151,16 @@ internal fun FileSystem.fileSequence( return result.asSequence() } +private fun Path.findCommonParentDir(path: Path): Path = + when { + path.startsWith(this) -> + this + startsWith(path) -> + path + else -> + this@findCommonParentDir.findCommonParentDir(path.parent) + } + private fun FileSystem.expand( patterns: List, rootDir: Path, diff --git a/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/internal/FileUtilsTest.kt b/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/internal/FileUtilsTest.kt index cfebaa39a9..f3e56b49c2 100644 --- a/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/internal/FileUtilsTest.kt +++ b/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/internal/FileUtilsTest.kt @@ -13,6 +13,7 @@ import org.junit.jupiter.api.condition.OS import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import java.nio.file.Path +import kotlin.io.path.absolutePathString private val LOGGER = KotlinLogging.logger {}.initKtLintKLogger() @@ -35,6 +36,7 @@ internal class FileUtilsTest { private val ktFile2InProjectSubDirectory = "project1/src/main/kotlin/example/Two.kt" private val ktsFileInProjectSubDirectory = "project1/src/scripts/Script.kts" private val javaFileInProjectSubDirectory = "project1/src/main/java/One.java" + private val someFileInOtherProjectRootDirectory = "other-project/SomeFile.txt" @BeforeEach internal fun setUp() { @@ -52,6 +54,7 @@ internal class FileUtilsTest { createFile(ktFile1InProjectSubDirectory) createFile(ktFile2InProjectSubDirectory) createFile(javaFileInProjectSubDirectory) + createFile(someFileInOtherProjectRootDirectory) } } @@ -414,6 +417,68 @@ internal class FileUtilsTest { ) } + @DisabledOnOs(OS.WINDOWS) + @Test + fun `Issue 2002 - On non-Windows OS, find files in a sibling directory based on a relative path to the working directory`() { + val foundFiles = + getFiles( + patterns = listOf("../project1"), + rootDir = ktlintTestFileSystem.resolve(someFileInOtherProjectRootDirectory).parent.toAbsolutePath(), + ) + + assertThat(foundFiles) + .containsExactlyInAnyOrder( + ktFileInProjectRootDirectory, + ktsFileInProjectRootDirectory, + ktFile1InProjectSubDirectory, + ktFile2InProjectSubDirectory, + ktsFileInProjectSubDirectory, + ).doesNotContain( + ktFileRootDirectory, + ktsFileRootDirectory, + ) + } + + @DisabledOnOs(OS.WINDOWS) + @Test + fun `Issue 2002 - On non-Windows OS, find files in a sibling directory based on a relative glob`() { + val foundFiles = + getFiles( + patterns = listOf("../project1/**/*.kt"), + rootDir = ktlintTestFileSystem.resolve("other-project"), + ) + + assertThat(foundFiles) + .containsExactlyInAnyOrder( + ktFileInProjectRootDirectory, + ktFile1InProjectSubDirectory, + ktFile2InProjectSubDirectory, + ).doesNotContain( + ktFileRootDirectory, + ) + } + + @Test + fun `Issue 2002 - Find files in a sibling directory based on an absolute path`() { + val foundFiles = + getFiles( + patterns = listOf(ktlintTestFileSystem.resolve(ktFileInProjectRootDirectory).parent.absolutePathString()), + rootDir = ktlintTestFileSystem.resolve(someFileInOtherProjectRootDirectory).parent.toAbsolutePath(), + ) + + assertThat(foundFiles) + .containsExactlyInAnyOrder( + ktFileInProjectRootDirectory, + ktsFileInProjectRootDirectory, + ktFile1InProjectSubDirectory, + ktFile2InProjectSubDirectory, + ktsFileInProjectSubDirectory, + ).doesNotContain( + ktFileRootDirectory, + ktsFileRootDirectory, + ) + } + private fun KtlintTestFileSystem.createFile(fileName: String) = writeFile( relativeDirectoryToRoot = fileName.substringBeforeLast("/", ""), From 930d2636da09ddf394078eacf53da93e05a6b182 Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Tue, 9 May 2023 12:44:57 +0200 Subject: [PATCH 39/80] Extract rule `no-single-line-block-comment` from `comment-wrapping` rule (#2000) Closes #1980 --- CHANGELOG.md | 1 + docs/rules/experimental.md | 26 +++ docs/rules/standard.md | 2 +- .../standard/StandardRuleSetProvider.kt | 2 + .../standard/rules/CommentWrappingRule.kt | 35 +--- .../rules/NoSingleLineBlockCommentRule.kt | 81 ++++++++ .../standard/rules/CommentWrappingRuleTest.kt | 129 ------------- .../rules/NoSingleLineBlockCommentRuleTest.kt | 179 ++++++++++++++++++ 8 files changed, 291 insertions(+), 164 deletions(-) create mode 100644 ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRule.kt create mode 100644 ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRuleTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 7665bcc167..1786caf0d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). * Prevent nullpointer exception (NPE) if or operator at start of line is followed by dot qualified expression `indent` ([#1993](https://github.com/pinterest/ktlint/issues/1993)) * Fix indentation of multiline parameter list in function literal `indent` ([#1976](https://github.com/pinterest/ktlint/issues/1976)) * Restrict indentation of closing quotes to `ktlint_official` code style to keep formatting of other code styles consistent with `0.48.x` and before `indent` ([#1971](https://github.com/pinterest/ktlint/issues/1971)) +* Extract rule `no-single-line-block-comment` from `comment-wrapping` rule. The `no-single-line-block-comment` rule is added as experimental rule to the `ktlint_official` code style, but it can be enabled explicitly for the other code styles as well. ([#1980](https://github.com/pinterest/ktlint/issues/1980)) * Clean-up unwanted logging dependencies ([#1998](https://github.com/pinterest/ktlint/issues/1998)) * Fix directory traversal for patterns referring to paths outside of current working directory or any of it child directories ([#2002](https://github.com/pinterest/ktlint/issues/2002)) diff --git a/docs/rules/experimental.md b/docs/rules/experimental.md index 7aa9f1dd3a..61561d524b 100644 --- a/docs/rules/experimental.md +++ b/docs/rules/experimental.md @@ -345,6 +345,32 @@ This rule can also be suppressed with the IntelliJ IDEA inspection suppression ` Rule id: `property-naming` +## No single line block comments + +A single line block comment should be replaced with an EOL comment when possible. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + /* + * Some comment + */ + val foo = "foo" // Some comment + val foo = { /* no-op */ } + + /* ktlint-disable foo-rule-id bar-rule-id */ + val foo = "foo" + /* ktlint-enable foo-rule-id bar-rule-id */ + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + /* Some comment */ + val foo = "foo" /* Some comment */ + ``` + +Rule id: `no-single-line-block-comment` + ## Spacing ### No blank lines in list diff --git a/docs/rules/standard.md b/docs/rules/standard.md index e64adc0011..7ba6b283bc 100644 --- a/docs/rules/standard.md +++ b/docs/rules/standard.md @@ -867,7 +867,7 @@ Rule id: `wrapping` ### Comment wrapping -A block comment should start and end on a line that does not contain any other element. A block comment should not be used as end of line comment. +A block comment should start and end on a line that does not contain any other element. === "[:material-heart:](#) Ktlint" diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt index 6496a9da29..04c51c4aeb 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt @@ -45,6 +45,7 @@ import com.pinterest.ktlint.ruleset.standard.rules.NoLineBreakAfterElseRule import com.pinterest.ktlint.ruleset.standard.rules.NoLineBreakBeforeAssignmentRule import com.pinterest.ktlint.ruleset.standard.rules.NoMultipleSpacesRule import com.pinterest.ktlint.ruleset.standard.rules.NoSemicolonsRule +import com.pinterest.ktlint.ruleset.standard.rules.NoSingleLineBlockCommentRule import com.pinterest.ktlint.ruleset.standard.rules.NoTrailingSpacesRule import com.pinterest.ktlint.ruleset.standard.rules.NoUnitReturnRule import com.pinterest.ktlint.ruleset.standard.rules.NoUnusedImportsRule @@ -126,6 +127,7 @@ public class StandardRuleSetProvider : RuleProvider { NoLineBreakBeforeAssignmentRule() }, RuleProvider { NoMultipleSpacesRule() }, RuleProvider { NoSemicolonsRule() }, + RuleProvider { NoSingleLineBlockCommentRule() }, RuleProvider { NoTrailingSpacesRule() }, RuleProvider { NoUnitReturnRule() }, RuleProvider { NoUnusedImportsRule() }, diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRule.kt index d08f75668d..24998e9685 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRule.kt @@ -1,7 +1,6 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.ElementType.BLOCK_COMMENT -import com.pinterest.ktlint.rule.engine.core.api.ElementType.EOL_COMMENT import com.pinterest.ktlint.rule.engine.core.api.ElementType.LBRACE import com.pinterest.ktlint.rule.engine.core.api.ElementType.RBRACE import com.pinterest.ktlint.rule.engine.core.api.RuleId @@ -18,13 +17,10 @@ import com.pinterest.ktlint.rule.engine.core.api.prevLeaf import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceBeforeMe import com.pinterest.ktlint.ruleset.standard.StandardRule import org.jetbrains.kotlin.com.intellij.lang.ASTNode -import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement -import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiCommentImpl import org.jetbrains.kotlin.psi.psiUtil.leaves /** - * Checks external wrapping of block comments. Wrapping inside the comment is not altered. A block comment following another element on the - * same line is replaced with an EOL comment, if possible. + * Checks external wrapping of block comments. Wrapping inside the comment is not altered. */ public class CommentWrappingRule : StandardRule( @@ -54,17 +50,6 @@ public class CommentWrappingRule : .firstOrNull() ?: node.lastChildLeafOrSelf() - if (!node.textContains('\n') && - !node.isKtlintSuppressionDirective() && - beforeBlockComment.prevLeaf().isWhitespaceWithNewlineOrNull() && - afterBlockComment.nextLeaf().isWhitespaceWithNewlineOrNull() - ) { - emit(node.startOffset, "A single line block comment must be replaced with an EOL comment", true) - if (autoCorrect) { - node.replaceWithEndOfLineComment() - } - } - if (!beforeBlockComment.prevLeaf().isWhitespaceWithNewlineOrNull() && !afterBlockComment.nextLeaf().isWhitespaceWithNewlineOrNull() ) { @@ -112,7 +97,6 @@ public class CommentWrappingRule : ) if (autoCorrect) { node.upsertWhitespaceBeforeMe(" ") - node.replaceWithEndOfLineComment() } } } @@ -133,24 +117,7 @@ public class CommentWrappingRule : } } - private fun ASTNode.replaceWithEndOfLineComment() { - val content = text.removeSurrounding("/*", "*/").trim() - val eolComment = PsiCommentImpl(EOL_COMMENT, "// $content") - (this as LeafPsiElement).rawInsertBeforeMe(eolComment) - rawRemove() - } - private fun ASTNode?.isWhitespaceWithNewlineOrNull() = this == null || this.isWhiteSpaceWithNewline() - - // TODO: Remove when ktlint suppression directive in comments are no longer supported - private fun ASTNode?.isKtlintSuppressionDirective() = - this - ?.text - ?.removePrefix("/*") - ?.removeSuffix("*/") - ?.trim() - ?.let { it.startsWith("ktlint-enable") || it.startsWith("ktlint-disable") } - ?: false } public val COMMENT_WRAPPING_RULE_ID: RuleId = CommentWrappingRule().ruleId diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRule.kt new file mode 100644 index 0000000000..08b99363bc --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRule.kt @@ -0,0 +1,81 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.rule.engine.core.api.ElementType.BLOCK_COMMENT +import com.pinterest.ktlint.rule.engine.core.api.ElementType.EOL_COMMENT +import com.pinterest.ktlint.rule.engine.core.api.Rule +import com.pinterest.ktlint.rule.engine.core.api.Rule.VisitorModifier.RunAfterRule +import com.pinterest.ktlint.rule.engine.core.api.Rule.VisitorModifier.RunAfterRule.Mode.REGARDLESS_WHETHER_RUN_AFTER_RULE_IS_LOADED_OR_DISABLED +import com.pinterest.ktlint.rule.engine.core.api.RuleId +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline +import com.pinterest.ktlint.rule.engine.core.api.lastChildLeafOrSelf +import com.pinterest.ktlint.rule.engine.core.api.nextLeaf +import com.pinterest.ktlint.ruleset.standard.StandardRule +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiCommentImpl +import org.jetbrains.kotlin.psi.psiUtil.leaves + +/** + * A block comment following another element on the same line is replaced with an EOL comment, if possible. + */ +public class NoSingleLineBlockCommentRule : + StandardRule( + id = "no-single-line-block-comment", + usesEditorConfigProperties = + setOf( + INDENT_SIZE_PROPERTY, + INDENT_STYLE_PROPERTY, + ), + visitorModifiers = setOf(RunAfterRule(COMMENT_WRAPPING_RULE_ID, REGARDLESS_WHETHER_RUN_AFTER_RULE_IS_LOADED_OR_DISABLED)), + ), + Rule.Experimental, + Rule.OfficialCodeStyle { + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + if (node.elementType == BLOCK_COMMENT) { + val afterBlockComment = + node + .leaves() + .takeWhile { it.isWhiteSpace() && !it.textContains('\n') } + .firstOrNull() + ?: node.lastChildLeafOrSelf() + + if (!node.textContains('\n') && + !node.isKtlintSuppressionDirective() && + afterBlockComment.nextLeaf().isWhitespaceWithNewlineOrNull() + ) { + emit(node.startOffset, "Replace the block comment with an EOL comment", true) + if (autoCorrect) { + node.replaceWithEndOfLineComment() + } + } + } + } + + private fun ASTNode.replaceWithEndOfLineComment() { + val content = text.removeSurrounding("/*", "*/").trim() + val eolComment = PsiCommentImpl(EOL_COMMENT, "// $content") + (this as LeafPsiElement).rawInsertBeforeMe(eolComment) + rawRemove() + } + + private fun ASTNode?.isWhitespaceWithNewlineOrNull() = this == null || this.isWhiteSpaceWithNewline() + + // TODO: Remove when ktlint suppression directive in comments are no longer supported + private fun ASTNode?.isKtlintSuppressionDirective() = + this + ?.text + ?.removePrefix("/*") + ?.removeSuffix("*/") + ?.trim() + ?.let { it.startsWith("ktlint-enable") || it.startsWith("ktlint-disable") } + ?: false +} + +public val NO_SINGLE_LINE_BLOCK_COMMENT_RULE_ID: RuleId = NoSingleLineBlockCommentRule().ruleId diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRuleTest.kt index 6ee2f51716..4ca97cf275 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/CommentWrappingRuleTest.kt @@ -1,7 +1,5 @@ package com.pinterest.ktlint.ruleset.standard.rules -import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY -import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue.ktlint_official import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -9,26 +7,6 @@ import org.junit.jupiter.api.Test class CommentWrappingRuleTest { private val commentWrappingRuleAssertThat = assertThatRule { CommentWrappingRule() } - @Test - fun `Given a single line block comment then replace it with an EOL comment`() { - val code = - """ - fun bar() { - /* Some comment */ - } - """.trimIndent() - val formattedCode = - """ - fun bar() { - // Some comment - } - """.trimIndent() - commentWrappingRuleAssertThat(code) - .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) - .hasLintViolation(2, 5, "A single line block comment must be replaced with an EOL comment") - .isFormattedAs(formattedCode) - } - @Test fun `Given a multi line block comment that start starts and end on a separate line then do not reformat`() { val code = @@ -107,73 +85,6 @@ class CommentWrappingRuleTest { } } - @Nested - inner class `Given some code code followed by a block comment on the same line` { - @Test - fun `Given a comment followed by a property and separated with space`() { - val code = - """ - val foo = "foo" /* Some comment */ - """.trimIndent() - val formattedCode = - """ - val foo = "foo" // Some comment - """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") - commentWrappingRuleAssertThat(code) - .hasLintViolation(1, 17, "A single line block comment after a code element on the same line must be replaced with an EOL comment") - .isFormattedAs(formattedCode) - } - - @Test - fun `Given a comment followed by a property but not separated with space`() { - val code = - """ - val foo = "foo"/* Some comment */ - """.trimIndent() - val formattedCode = - """ - val foo = "foo" // Some comment - """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") - commentWrappingRuleAssertThat(code) - .hasLintViolation(1, 16, "A single line block comment after a code element on the same line must be replaced with an EOL comment") - .isFormattedAs(formattedCode) - } - - @Test - fun `Given a comment followed by a function and separated with space`() { - val code = - """ - fun foo() = "foo" /* Some comment */ - """.trimIndent() - val formattedCode = - """ - fun foo() = "foo" // Some comment - """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") - commentWrappingRuleAssertThat(code) - .hasLintViolation(1, 19, "A single line block comment after a code element on the same line must be replaced with an EOL comment") - .isFormattedAs(formattedCode) - } - - @Test - fun `Given a comment followed by a function but not separated with space`() { - val code = - """ - fun foo() = "foo"/* Some comment */ - """.trimIndent() - val formattedCode = - """ - fun foo() = "foo" // Some comment - """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") - commentWrappingRuleAssertThat(code) - .hasLintViolation(1, 18, "A single line block comment after a code element on the same line must be replaced with an EOL comment") - .isFormattedAs(formattedCode) - } - } - @Test fun `Given a block comment containing a newline which is preceded by another element on the same line then raise lint error but do not autocorrect`() { val code = @@ -187,22 +98,6 @@ class CommentWrappingRuleTest { .hasLintViolationWithoutAutoCorrect(1, 17, "A block comment after any other element on the same line must be separated by a new line") } - @Test - fun `Given a block comment that does not contain a newline and which is after some code om the same line is changed to an EOL comment`() { - val code = - """ - val foo = "foo" /* Some comment */ - """.trimIndent() - val formattedCode = - """ - val foo = "foo" // Some comment - """.trimIndent() - @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") - commentWrappingRuleAssertThat(code) - .hasLintViolation(1, 17, "A single line block comment after a code element on the same line must be replaced with an EOL comment") - .isFormattedAs(formattedCode) - } - @Test fun `Given a block comment in between code elements on the same line then raise lint error but do not autocorrect`() { val code = @@ -246,28 +141,4 @@ class CommentWrappingRuleTest { .hasLintViolation(2, 24, "A block comment may not be followed by any other element on that same line") .isFormattedAs(formattedCode) } - - @Test - fun `Given a single line block containing a block comment then do not reformat`() { - val code = - """ - val foo = { /* no-op */ } - """.trimIndent() - commentWrappingRuleAssertThat(code) - .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) - .hasNoLintViolations() - } - - @Test - fun `Given single line block comments to disable or enable ktlint then do not reformat`() { - val code = - """ - /* ktlint-disable foo-rule-id bar-rule-id */ - val foo = "foo" - /* ktlint-enable foo-rule-id bar-rule-id */ - """.trimIndent() - commentWrappingRuleAssertThat(code) - .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) - .hasNoLintViolations() - } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRuleTest.kt new file mode 100644 index 0000000000..68eefbd98b --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoSingleLineBlockCommentRuleTest.kt @@ -0,0 +1,179 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.rule.engine.core.api.RuleProvider +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue +import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class NoSingleLineBlockCommentRuleTest { + private val noSingleLineBlockCommentRuleAssertThat = + assertThatRule( + provider = { NoSingleLineBlockCommentRule() }, + additionalRuleProviders = setOf(RuleProvider { CommentWrappingRule() }), + editorConfigProperties = setOf(CODE_STYLE_PROPERTY to CodeStyleValue.ktlint_official), + ) + + @Test + fun `Given a single line block comment then replace it with an EOL comment`() { + val code = + """ + fun bar() { + /* Some comment */ + } + """.trimIndent() + val formattedCode = + """ + fun bar() { + // Some comment + } + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to CodeStyleValue.ktlint_official) + .hasLintViolation(2, 5, "Replace the block comment with an EOL comment") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a multi line block comment that start starts and end on a separate line then do not reformat`() { + val code = + """ + /* + * Some comment + */ + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given a single line block comment that is to be wrapped before replacing it with an EOL comment`() { + val code = + """ + /* Some comment */ val foo = "foo" + """.trimIndent() + val formattedCode = + """ + // Some comment + val foo = "foo" + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code) + .hasLintViolationForAdditionalRule(1, 20, "A block comment may not be followed by any other element on that same line") + // Can not check for the lint violation below as it will only be thrown while formatting with comment wrapping + // A single line block comment must be replaced with an EOL comment + .isFormattedAs(formattedCode) + } + + @Nested + inner class `Given some code code followed by a block comment on the same line` { + @Test + fun `Given a comment followed by a property and separated with space`() { + val code = + """ + val foo = "foo" /* Some comment */ + """.trimIndent() + val formattedCode = + """ + val foo = "foo" // Some comment + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code) + .hasLintViolation(1, 17, "Replace the block comment with an EOL comment") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a comment followed by a property but not separated with space`() { + val code = + """ + val foo = "foo"/* Some comment */ + """.trimIndent() + val formattedCode = + """ + val foo = "foo" // Some comment + """.trimIndent() + @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + noSingleLineBlockCommentRuleAssertThat(code) + .hasLintViolation(1, 16, "Replace the block comment with an EOL comment") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a comment followed by a function and separated with space`() { + val code = + """ + fun foo() = "foo" /* Some comment */ + """.trimIndent() + val formattedCode = + """ + fun foo() = "foo" // Some comment + """.trimIndent() + @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") + noSingleLineBlockCommentRuleAssertThat(code) + .hasLintViolation(1, 19, "Replace the block comment with an EOL comment") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a comment followed by a function but not separated with space`() { + val code = + """ + fun foo() = "foo"/* Some comment */ + """.trimIndent() + val formattedCode = + """ + fun foo() = "foo" // Some comment + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code) + .hasLintViolation(1, 18, "Replace the block comment with an EOL comment") + .isFormattedAs(formattedCode) + } + } + + @Test + fun `Given a block comment that does not contain a newline and which is after some code om the same line is changed to an EOL comment`() { + val code = + """ + val foo = "foo" /* Some comment */ + """.trimIndent() + val formattedCode = + """ + val foo = "foo" // Some comment + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code) + .hasLintViolation(1, 17, "Replace the block comment with an EOL comment") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a single line block comment in between code elements on the same line does not raise a lint error`() { + val code = + """ + val foo /* some comment */ = "foo" + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code).hasNoLintViolationsExceptInAdditionalRules() + } + + @Test + fun `Given a single line block containing a block comment then do not reformat`() { + val code = + """ + val foo = { /* no-op */ } + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to CodeStyleValue.ktlint_official) + .hasNoLintViolations() + } + + @Test + fun `Given single line block comments to disable or enable ktlint then do not reformat`() { + val code = + """ + /* ktlint-disable foo-rule-id bar-rule-id */ + val foo = "foo" + /* ktlint-enable foo-rule-id bar-rule-id */ + """.trimIndent() + noSingleLineBlockCommentRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to CodeStyleValue.ktlint_official) + .hasNoLintViolations() + } +} From 321abad9ccd3f8814bc6e0f8aa51cd0953097fe0 Mon Sep 17 00:00:00 2001 From: Avinash M Date: Wed, 10 May 2023 21:17:38 +0530 Subject: [PATCH 40/80] Fix typos (#2007) --- docs/rules/standard.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/rules/standard.md b/docs/rules/standard.md index 7ba6b283bc..3bf94be6e5 100644 --- a/docs/rules/standard.md +++ b/docs/rules/standard.md @@ -247,13 +247,13 @@ Indentation formatting - respects `.editorconfig` `indent_size` with no continua ``` !!! note - This rule handles indentation for many different language constructs which can not be summarized with a few example. See the [unit tests](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt) for more details. + This rule handles indentation for many different language constructs which can not be summarized with a few examples. See the [unit tests](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt) for more details. Rule id: `indent` ## Max line length -Ensures that lines do not exceed the given length of `.editorconfig` property `max_line_length` (see [EditorConfig](../configuration-ktlint/) section for more). This rule does not apply in a number of situations. For example, in the case a line exceeds the maximum line length due to a comment that disables ktlint rules than that comment is being ignored when validating the length of the line. The `.editorconfig` property `ktlint_ignore_back_ticked_identifier` can be set to ignore identifiers which are enclosed in backticks, which for example is very useful when you want to allow longer names for unit tests. +Ensures that lines do not exceed the given length of `.editorconfig` property `max_line_length` (see [EditorConfig](../configuration-ktlint/) section for more). This rule does not apply in a number of situations. For example, in the case a line exceeds the maximum line length due to a comment that disables ktlint rules then that comment is being ignored when validating the length of the line. The `.editorconfig` property `ktlint_ignore_back_ticked_identifier` can be set to ignore identifiers which are enclosed in backticks, which for example is very useful when you want to allow longer names for unit tests. === "[:material-heart:](#) Ktlint" @@ -261,7 +261,7 @@ Ensures that lines do not exceed the given length of `.editorconfig` property `m // Assume that the last allowed character is // at the X character on the right X // Lines below are accepted although the max - // line length is exceeeded. + // line length is exceeded. package com.toooooooooooooooooooooooooooo.long import com.tooooooooooooooooooooooooooooo.long val foo = From cd1c78f051fc4fc31d945cf777c8dc174c2841ee Mon Sep 17 00:00:00 2001 From: Eliezer Graber Date: Wed, 10 May 2023 11:59:15 -0400 Subject: [PATCH 41/80] Release to SDKMan when publishing a release (#1978) --- .github/workflows/release.yml | 29 +++++++++++++++++++++++------ CHANGELOG.md | 2 ++ docs/install/cli.md | 5 +++++ gradle/libs.versions.toml | 1 + ktlint-cli/build.gradle.kts | 9 +++++++++ 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 61a206cc85..5011a337eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,6 +35,20 @@ jobs: if: ${{ success() }} uses : ffurrer2/extract-release-notes@v1 + - name: Get version + id: get_version + if: ${{ success() }} + run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV + + - name: Create zip for dependency managers + if: ${{ success() }} + run: | + cd ktlint-cli/build/run + # Doing this for Homebrew and https://github.com/sdkman/sdkman-cli/wiki/Well-formed-SDK-archives + mkdir -p ktlint-${{ env.version }}/bin + cp ktlint ktlint-${{ env.version }}/bin + zip -rm ktlint-${{ env.version }}.zip ktlint-${{ env.version }} + - name : Create release id: github_release if: ${{ success() }} @@ -48,11 +62,6 @@ jobs: env : GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }} - - name: Get version - id: get_version - if: ${{ success() }} - run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV - - name: Bump Homebrew Formula if: ${{ success() }} uses: mislav/bump-homebrew-formula-action@v2 @@ -60,7 +69,15 @@ jobs: COMMITTER_TOKEN: ${{ secrets.HOMEBREW_TOKEN }} with: formula-name: ktlint - download-url: https://github.com/pinterest/ktlint/releases/download/${{ env.version }}/ktlint + download-url: https://github.com/pinterest/ktlint/releases/download/${{ env.version }}/ktlint-${{ env.version }}.zip + + - name: Release to sdkman + if: ${{ success() }} + env: + SDKMAN_KEY: ${{ secrets.SDKMAN_KEY }} + SDKMAN_TOKEN: ${{ secrets.SDKMAN_TOKEN }} + SDKMAN_VERSION: ${{ env.version }} + run: ./gradlew :ktlint-cli:sdkMajorRelease - name: Announce Release if: ${{ success() }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1786caf0d8..19e228a4c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Added +* `ktlint` is now available on SDKMAN! + ### Removed ### Fixed diff --git a/docs/install/cli.md b/docs/install/cli.md index 7a6471aa29..59fd63ecf2 100644 --- a/docs/install/cli.md +++ b/docs/install/cli.md @@ -48,6 +48,11 @@ Install with [MacPorts](https://www.macports.org/) port install ktlint ``` +Install with [SDKMAN! on macOS and Linux](https://sdkman.io/) +```sh +sdk install ktlint +``` + On Arch Linux install package [ktlint AUR](https://aur.archlinux.org/packages/ktlint/). ## Command line usage diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6dbb750b4b..953dd97532 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ kotlinDev = "1.8.20" kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } checksum = "org.gradle.crypto.checksum:1.4.0" shadow = "com.github.johnrengelman.shadow:8.1.1" +sdkman = "io.sdkman.vendors:3.0.0" [libraries] kotlin-compiler = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } diff --git a/ktlint-cli/build.gradle.kts b/ktlint-cli/build.gradle.kts index c253fb1da9..41c9dbe0aa 100644 --- a/ktlint-cli/build.gradle.kts +++ b/ktlint-cli/build.gradle.kts @@ -4,6 +4,7 @@ plugins { id("ktlint-publication-library") alias(libs.plugins.shadow) alias(libs.plugins.checksum) + alias(libs.plugins.sdkman) signing } @@ -140,3 +141,11 @@ tasks.withType().configureEach { systemProperty("ktlint-version", ktlintVersion) } } + +sdkman { + val sdkmanVersion = providers.environmentVariable("SDKMAN_KEY").orElse(project.version.toString()) + candidate.set("ktlint") + version.set(sdkmanVersion) + url.set("https://github.com/pinterest/ktlint/releases/download/$sdkmanVersion/ktlint-$sdkmanVersion.zip") + hashtag.set("ktlint") +} From 7d38d3df09e1d2638b36f24327ff93bf90e8565a Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Wed, 10 May 2023 21:18:50 +0200 Subject: [PATCH 42/80] Configure versioned documentation (#2008) This allows to distinguish between documentation changes which related to the latest published released version documentation that is only applicable to the lastest snapshot build. Closes #1932 --- .announce | 93 +- .github/workflows/gradle-docs-publish.yml | 24 +- .github/workflows/gradle-pr-build.yml | 4 +- .github/workflows/release-docs.yml | 39 + .github/workflows/release.yml | 3 +- documentation/readme.md | 15 + .../release-latest/docs}/api/badge.md | 0 .../docs}/api/custom-integration.md | 0 .../docs}/api/custom-reporter.md | 0 .../docs}/api/custom-rule-set.md | 0 .../release-latest/docs}/api/index.md | 0 .../release-latest/docs}/api/overview.md | 0 .../docs}/assets/images/favicon.ico | Bin .../assets/images/module-dependencies.png | Bin .../docs}/assets/images/psi-viewer.png | Bin .../docs}/assets/images/rule-dependencies.png | Bin .../docs}/contributing/code-of-conduct.md | 0 .../docs}/contributing/guidelines.md | 0 .../docs}/contributing/index.md | 0 .../docs}/contributing/overview.md | 0 .../release-latest/docs}/faq.md | 0 .../release-latest/docs}/index.md | 0 .../release-latest/docs}/install/cli.md | 0 .../release-latest/docs}/install/index.md | 0 .../docs}/install/integrations.md | 0 .../release-latest/docs}/install/overview.md | 0 .../docs}/install/snapshot-build.md | 0 .../release-latest/docs}/readme.md | 0 .../release-latest/docs}/rules/code-styles.md | 0 .../rules/configuration-intellij-idea.md | 0 .../docs}/rules/configuration-ktlint.md | 0 .../docs}/rules/dependencies.md | 0 .../docs}/rules/experimental.md | 0 .../release-latest/docs}/rules/index.md | 0 .../release-latest/docs}/rules/standard.md | 0 .../release-latest/mkdocs.yml | 8 + .../release-latest/overrides/main.html | 8 + .../release-latest/run-mkdocs-server.sh | 0 documentation/snapshot/docs/api/badge.md | 6 + .../snapshot/docs/api/custom-integration.md | 116 ++ .../snapshot/docs/api/custom-reporter.md | 18 + .../snapshot/docs/api/custom-rule-set.md | 73 + documentation/snapshot/docs/api/index.md | 0 documentation/snapshot/docs/api/overview.md | 16 + .../snapshot/docs/assets/images/favicon.ico | Bin 0 -> 15086 bytes .../assets/images/module-dependencies.png | Bin 0 -> 119648 bytes .../docs/assets/images/psi-viewer.png | Bin 0 -> 79239 bytes .../docs/assets/images/rule-dependencies.png | Bin 0 -> 43730 bytes .../docs/contributing/code-of-conduct.md | 27 + .../snapshot/docs/contributing/guidelines.md | 59 + .../snapshot/docs/contributing/index.md | 1 + .../snapshot/docs/contributing/overview.md | 19 + documentation/snapshot/docs/faq.md | 186 +++ documentation/snapshot/docs/index.md | 41 + documentation/snapshot/docs/install/cli.md | 222 +++ documentation/snapshot/docs/install/index.md | 0 .../snapshot/docs/install/integrations.md | 225 +++ .../snapshot/docs/install/overview.md | 6 + .../snapshot/docs/install/snapshot-build.md | 37 + documentation/snapshot/docs/readme.md | 32 + .../snapshot/docs/rules/code-styles.md | 17 + .../docs/rules/configuration-intellij-idea.md | 36 + .../docs/rules/configuration-ktlint.md | 258 +++ .../snapshot/docs/rules/dependencies.md | 3 + .../snapshot/docs/rules/experimental.md | 797 +++++++++ documentation/snapshot/docs/rules/index.md | 1 + documentation/snapshot/docs/rules/standard.md | 1424 +++++++++++++++++ documentation/snapshot/mkdocs.yml | 90 ++ documentation/snapshot/overrides/main.html | 12 + documentation/snapshot/run-mkdocs-server.sh | 5 + 70 files changed, 3890 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/release-docs.yml create mode 100644 documentation/readme.md rename {docs => documentation/release-latest/docs}/api/badge.md (100%) rename {docs => documentation/release-latest/docs}/api/custom-integration.md (100%) rename {docs => documentation/release-latest/docs}/api/custom-reporter.md (100%) rename {docs => documentation/release-latest/docs}/api/custom-rule-set.md (100%) rename {docs => documentation/release-latest/docs}/api/index.md (100%) rename {docs => documentation/release-latest/docs}/api/overview.md (100%) rename {docs => documentation/release-latest/docs}/assets/images/favicon.ico (100%) rename {docs => documentation/release-latest/docs}/assets/images/module-dependencies.png (100%) rename {docs => documentation/release-latest/docs}/assets/images/psi-viewer.png (100%) rename {docs => documentation/release-latest/docs}/assets/images/rule-dependencies.png (100%) rename {docs => documentation/release-latest/docs}/contributing/code-of-conduct.md (100%) rename {docs => documentation/release-latest/docs}/contributing/guidelines.md (100%) rename {docs => documentation/release-latest/docs}/contributing/index.md (100%) rename {docs => documentation/release-latest/docs}/contributing/overview.md (100%) rename {docs => documentation/release-latest/docs}/faq.md (100%) rename {docs => documentation/release-latest/docs}/index.md (100%) rename {docs => documentation/release-latest/docs}/install/cli.md (100%) rename {docs => documentation/release-latest/docs}/install/index.md (100%) rename {docs => documentation/release-latest/docs}/install/integrations.md (100%) rename {docs => documentation/release-latest/docs}/install/overview.md (100%) rename {docs => documentation/release-latest/docs}/install/snapshot-build.md (100%) rename {docs => documentation/release-latest/docs}/readme.md (100%) rename {docs => documentation/release-latest/docs}/rules/code-styles.md (100%) rename {docs => documentation/release-latest/docs}/rules/configuration-intellij-idea.md (100%) rename {docs => documentation/release-latest/docs}/rules/configuration-ktlint.md (100%) rename {docs => documentation/release-latest/docs}/rules/dependencies.md (100%) rename {docs => documentation/release-latest/docs}/rules/experimental.md (100%) rename {docs => documentation/release-latest/docs}/rules/index.md (100%) rename {docs => documentation/release-latest/docs}/rules/standard.md (100%) rename mkdocs.yml => documentation/release-latest/mkdocs.yml (95%) create mode 100644 documentation/release-latest/overrides/main.html rename run-mkdocs-server.sh => documentation/release-latest/run-mkdocs-server.sh (100%) create mode 100644 documentation/snapshot/docs/api/badge.md create mode 100644 documentation/snapshot/docs/api/custom-integration.md create mode 100644 documentation/snapshot/docs/api/custom-reporter.md create mode 100644 documentation/snapshot/docs/api/custom-rule-set.md create mode 100644 documentation/snapshot/docs/api/index.md create mode 100644 documentation/snapshot/docs/api/overview.md create mode 100644 documentation/snapshot/docs/assets/images/favicon.ico create mode 100644 documentation/snapshot/docs/assets/images/module-dependencies.png create mode 100644 documentation/snapshot/docs/assets/images/psi-viewer.png create mode 100644 documentation/snapshot/docs/assets/images/rule-dependencies.png create mode 100644 documentation/snapshot/docs/contributing/code-of-conduct.md create mode 100644 documentation/snapshot/docs/contributing/guidelines.md create mode 100644 documentation/snapshot/docs/contributing/index.md create mode 100644 documentation/snapshot/docs/contributing/overview.md create mode 100644 documentation/snapshot/docs/faq.md create mode 100644 documentation/snapshot/docs/index.md create mode 100644 documentation/snapshot/docs/install/cli.md create mode 100644 documentation/snapshot/docs/install/index.md create mode 100644 documentation/snapshot/docs/install/integrations.md create mode 100644 documentation/snapshot/docs/install/overview.md create mode 100644 documentation/snapshot/docs/install/snapshot-build.md create mode 100644 documentation/snapshot/docs/readme.md create mode 100644 documentation/snapshot/docs/rules/code-styles.md create mode 100644 documentation/snapshot/docs/rules/configuration-intellij-idea.md create mode 100644 documentation/snapshot/docs/rules/configuration-ktlint.md create mode 100644 documentation/snapshot/docs/rules/dependencies.md create mode 100644 documentation/snapshot/docs/rules/experimental.md create mode 100644 documentation/snapshot/docs/rules/index.md create mode 100644 documentation/snapshot/docs/rules/standard.md create mode 100644 documentation/snapshot/mkdocs.yml create mode 100644 documentation/snapshot/overrides/main.html create mode 100755 documentation/snapshot/run-mkdocs-server.sh diff --git a/.announce b/.announce index 26e04aae5e..fc6da86b75 100755 --- a/.announce +++ b/.announce @@ -1,7 +1,10 @@ #!/bin/bash -e # project.version -if [ "$VERSION" == "" ]; then exit 1; fi +if [ "$VERSION" == "" ]; then + echo "VERSION has not been set" + exit 1; +fi if [ "$1" = "-y" ]; then AUTOACCEPT=true @@ -23,43 +26,89 @@ fi echo "Announcing $PREVIOUS_VERSION -> $VERSION" -COMMIT_MESSAGE="Updated refs to latest ($VERSION) release" +DOCUMENTATION_DIR="documentation" +RELEASE_DOCS_DIR="${DOCUMENTATION_DIR}/release-latest" +SNAPSHOT_DOCS_DIR="${DOCUMENTATION_DIR}/snapshot" -if [ "$(git status --porcelain=v1 docs/install/cli.md docs/install/integrations.md)" != "" ]; then - echo "ERROR: To proceed, cli.md and integrations.md must not contain uncommitted changes" +if [ "$(git status --porcelain=v1 $DOCUMENTATION)" != "" ]; then + echo "ERROR: To proceed, the current branch must not contain uncommitted changes in directory '${DOCUMENTATION_DIR}'" # ask for user confirmation if [[ "$AUTOACCEPT" = false ]]; then - read -p "revert changes? (y/n)? " -n 1 -r; echo; if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1; else git checkout docs/install/cli.md docs/install/integrations.md; fi + read -p "revert changes? (y/n)? " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + git checkout ${DOCUMENTATION_DIR} + else + exit 1 + fi else - echo "Reverting changes to cli.md and integrations.md" - git checkout docs/install/cli.md docs/install/integrations.md + echo "Reverting changes in directory '${DOCUMENTATION_DIR}'" + git checkout ${DOCUMENTATION_DIR} fi fi -escape_for_sed() { echo "$1" | sed -e 's/[]\/$*.^|[]/\\&/g'; } +#escape_for_sed() { echo "$1" | sed -e 's/[]\/$*.^|[]/\\&/g'; } -# update Docs - -sed -i -e "s/$PREVIOUS_VERSION/$VERSION/g" docs/install/cli.md -sed -i -e "s/$PREVIOUS_VERSION/$VERSION/g" docs/install/integrations.md -git --no-pager diff docs/install/cli.md docs/install/integrations.md - -# ask for user confirmation -if [[ "$AUTOACCEPT" = false ]]; then - read -p "commit & push (y/n)? " -n 1 -r; echo; if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1; fi -fi +# Make a separate branch because master branch is protected BRANCH="$VERSION-update-refs" - if [ "$(git show-ref refs/heads/$BRANCH)" != "" ]; then echo "ERROR: Branch $BRANCH already exists." if [[ "$AUTOACCEPT" = false ]]; then - read -p "Delete local branch? (y/n)? " -n 1 -r; echo; if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1; else git branch -D $BRANCH; fi + read -p "Delete local branch? (y/n)? " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + else + git branch -D $BRANCH + fi else echo "Deleting local branch $BRANCH" git branch -D $BRANCH fi + # Checkout local master branch so that changes to this script can be tested without pushing the script to the remote branch + git checkout --track master -b $BRANCH +else + git checkout --track origin/master -b $BRANCH fi -# Make a separate branch because master branch is protected -git checkout --track origin/master -b $BRANCH && git commit -m "$COMMIT_MESSAGE" docs/install/cli.md docs/install/integrations.md && git push origin $BRANCH +# update version number in snapshot docs + +echo "Updating version numbers in (snapshot) installation documentation" +# Use "sed -i '' ..." instead of "sed -i -e ..." as the latter creates a new file on OSX +sed -i '' "s/$PREVIOUS_VERSION/$VERSION/g" ${SNAPSHOT_DOCS_DIR}/docs/install/cli.md +sed -i '' "s/$PREVIOUS_VERSION/$VERSION/g" ${SNAPSHOT_DOCS_DIR}/docs/install/integrations.md +git --no-pager diff ${DOCUMENTATION_DIR} + +# ask for user confirmation before committing +if [[ "$AUTOACCEPT" = false ]]; then + read -p "Accept changes (y/n)? " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +fi + +# Replace release documentation with current snapshot documentation. + +echo "Replace release documentation with current snapshot documentation" +# Support removal of files which still exists in release docs but which are no longer present in snapshot docs +rm -rf ${RELEASE_DOCS_DIR}/docs/ +cp -r ${SNAPSHOT_DOCS_DIR}/docs/ ${RELEASE_DOCS_DIR}/docs/ +# Note that directory "${SNAPSHOT_DOCS_DIR}/overrides/" should not replace "${RELEASE_DOCS_DIR}/overrides/" +cp -r ${SNAPSHOT_DOCS_DIR}/mkdocs.yml ${RELEASE_DOCS_DIR} +# Add files which previously did not yet exists in the release docs but were present in the snapshot docs +git add --all +# Display sorted list of files changed but do not show contents as that could be a lot +git status --porcelain=v1 documentation | sort + +# Commit and push changes +if [[ "$AUTOACCEPT" = false ]]; then + read -p "Commit and push changes (y/n)? " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +fi +git commit -m "Updated refs to latest ($VERSION) release" +git push origin $BRANCH diff --git a/.github/workflows/gradle-docs-publish.yml b/.github/workflows/gradle-docs-publish.yml index 04d58f0263..556be25254 100644 --- a/.github/workflows/gradle-docs-publish.yml +++ b/.github/workflows/gradle-docs-publish.yml @@ -1,16 +1,30 @@ -name: Publish documentation +name: Publish snapshot documentation on: push: - branches: - - master + branches: ['master'] + paths: ['documentation/snapshot/**'] jobs: deploy: runs-on: ubuntu-latest if: github.repository == 'pinterest/ktlint' steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 # fetch all commits/branches - uses: actions/setup-python@v4 with: python-version: 3.x - - run: pip install mkdocs-material - - run: mkdocs gh-deploy --force + + - name: Install mkdocs and mike + run: pip install mkdocs-material mike + + - name: Config git + run: | + git config user.email "ktlint@github.com" + git config user.name "Ktlint Release Workflow" + + - run: | + cd documentation/snapshot + # The dev-snapshot version has no release number as the version will only be tagged when an official release is build + # This also prevents that multiple snapshot versions of the documentation will be published in parallel + mike deploy --push dev-snapshot diff --git a/.github/workflows/gradle-pr-build.yml b/.github/workflows/gradle-pr-build.yml index 74d155ef82..f2d9169a42 100644 --- a/.github/workflows/gradle-pr-build.yml +++ b/.github/workflows/gradle-pr-build.yml @@ -2,8 +2,8 @@ name: PR Build on: push: - branches: - - master + branches: ['master'] + paths: ['**/*.kt', '**/*.kts', '**/*.properties', '**/*.toml'] pull_request: workflow_dispatch: diff --git a/.github/workflows/release-docs.yml b/.github/workflows/release-docs.yml new file mode 100644 index 0000000000..2f46b129b1 --- /dev/null +++ b/.github/workflows/release-docs.yml @@ -0,0 +1,39 @@ +name: Publish release documentation +on: + push: + branches: ['master'] + paths: ['documentation/release-latest/**'] +jobs: + deploy: + runs-on: ubuntu-latest + if: github.repository == 'pinterest/ktlint' + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # fetch all commits/branches + - uses: actions/setup-python@v4 + with: + python-version: 3.x + + - name: Install mkdocs and mike + run: pip install mkdocs-material mike + + - name: Config git + run: | + git config user.email "ktlint@github.com" + git config user.name "Ktlint Release Workflow" + + - name: Get last released version + run: echo "version=$(git describe --abbrev=0 --tags)" >> $GITHUB_ENV + + - name: Deploy release docs + run: | + echo "Deploy release docs to version ${{ env.version }}" + cd documentation/release-latest + # Release docs are versioned so that user can use relevant docs for the ktlint version they use + mike deploy --push --update-aliases ${{ env.version }} latest + + - name: Update default release docs + run: | + cd documentation/release-latest + mike set-default --push latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5011a337eb..c95a8cf618 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,7 +79,7 @@ jobs: SDKMAN_VERSION: ${{ env.version }} run: ./gradlew :ktlint-cli:sdkMajorRelease - - name: Announce Release + - name: Update Release documentation if: ${{ success() }} run: | git config user.email "<>" | @@ -87,4 +87,3 @@ jobs: ./.announce -y env: VERSION: ${{ env.version }} - diff --git a/documentation/readme.md b/documentation/readme.md new file mode 100644 index 0000000000..6073730275 --- /dev/null +++ b/documentation/readme.md @@ -0,0 +1,15 @@ +# Documentation + +Two versions of the documentation are kept in the 'master' branch: + +* The `snapshot` version of the documentation applies to the SNAPSHOT versions of ktlint. Upon the publication of the next official release of ktlint, the `release-latest` version of the documentation is replaced with the `snapshot` version. +* The `release-latest` version of the documentation applies to the last officially published version of ktlint. Upon the publication of the next official release of ktlint, this version of the documentation is replaced with the `snapshot` version. + +Whenever a fix is to be made to the documentation, it should be determined in which version(s) of the documentation is to be fixed. Documentation fixes which only apply to the `SNAPSHOT` version of ktlint may only be fixed in the `snapshot` version of the documentation. + +All other kind of documentation fixes most likely needs to be fixed in both the `snapshot` and `release-latest` versions. Only fixing it in `release-latest` may result in the fix being lost upon publication of the next official ktlint version. + + +Docs can be viewed on the local machine in one of following ways: +* Run script `serve-docs-locally.sh` which starts a docker container running mkdocs +* Run command `mike serve` which requires that `python`, `pip`, `mike` and `mkdocs` have been installed (one time only) before diff --git a/docs/api/badge.md b/documentation/release-latest/docs/api/badge.md similarity index 100% rename from docs/api/badge.md rename to documentation/release-latest/docs/api/badge.md diff --git a/docs/api/custom-integration.md b/documentation/release-latest/docs/api/custom-integration.md similarity index 100% rename from docs/api/custom-integration.md rename to documentation/release-latest/docs/api/custom-integration.md diff --git a/docs/api/custom-reporter.md b/documentation/release-latest/docs/api/custom-reporter.md similarity index 100% rename from docs/api/custom-reporter.md rename to documentation/release-latest/docs/api/custom-reporter.md diff --git a/docs/api/custom-rule-set.md b/documentation/release-latest/docs/api/custom-rule-set.md similarity index 100% rename from docs/api/custom-rule-set.md rename to documentation/release-latest/docs/api/custom-rule-set.md diff --git a/docs/api/index.md b/documentation/release-latest/docs/api/index.md similarity index 100% rename from docs/api/index.md rename to documentation/release-latest/docs/api/index.md diff --git a/docs/api/overview.md b/documentation/release-latest/docs/api/overview.md similarity index 100% rename from docs/api/overview.md rename to documentation/release-latest/docs/api/overview.md diff --git a/docs/assets/images/favicon.ico b/documentation/release-latest/docs/assets/images/favicon.ico similarity index 100% rename from docs/assets/images/favicon.ico rename to documentation/release-latest/docs/assets/images/favicon.ico diff --git a/docs/assets/images/module-dependencies.png b/documentation/release-latest/docs/assets/images/module-dependencies.png similarity index 100% rename from docs/assets/images/module-dependencies.png rename to documentation/release-latest/docs/assets/images/module-dependencies.png diff --git a/docs/assets/images/psi-viewer.png b/documentation/release-latest/docs/assets/images/psi-viewer.png similarity index 100% rename from docs/assets/images/psi-viewer.png rename to documentation/release-latest/docs/assets/images/psi-viewer.png diff --git a/docs/assets/images/rule-dependencies.png b/documentation/release-latest/docs/assets/images/rule-dependencies.png similarity index 100% rename from docs/assets/images/rule-dependencies.png rename to documentation/release-latest/docs/assets/images/rule-dependencies.png diff --git a/docs/contributing/code-of-conduct.md b/documentation/release-latest/docs/contributing/code-of-conduct.md similarity index 100% rename from docs/contributing/code-of-conduct.md rename to documentation/release-latest/docs/contributing/code-of-conduct.md diff --git a/docs/contributing/guidelines.md b/documentation/release-latest/docs/contributing/guidelines.md similarity index 100% rename from docs/contributing/guidelines.md rename to documentation/release-latest/docs/contributing/guidelines.md diff --git a/docs/contributing/index.md b/documentation/release-latest/docs/contributing/index.md similarity index 100% rename from docs/contributing/index.md rename to documentation/release-latest/docs/contributing/index.md diff --git a/docs/contributing/overview.md b/documentation/release-latest/docs/contributing/overview.md similarity index 100% rename from docs/contributing/overview.md rename to documentation/release-latest/docs/contributing/overview.md diff --git a/docs/faq.md b/documentation/release-latest/docs/faq.md similarity index 100% rename from docs/faq.md rename to documentation/release-latest/docs/faq.md diff --git a/docs/index.md b/documentation/release-latest/docs/index.md similarity index 100% rename from docs/index.md rename to documentation/release-latest/docs/index.md diff --git a/docs/install/cli.md b/documentation/release-latest/docs/install/cli.md similarity index 100% rename from docs/install/cli.md rename to documentation/release-latest/docs/install/cli.md diff --git a/docs/install/index.md b/documentation/release-latest/docs/install/index.md similarity index 100% rename from docs/install/index.md rename to documentation/release-latest/docs/install/index.md diff --git a/docs/install/integrations.md b/documentation/release-latest/docs/install/integrations.md similarity index 100% rename from docs/install/integrations.md rename to documentation/release-latest/docs/install/integrations.md diff --git a/docs/install/overview.md b/documentation/release-latest/docs/install/overview.md similarity index 100% rename from docs/install/overview.md rename to documentation/release-latest/docs/install/overview.md diff --git a/docs/install/snapshot-build.md b/documentation/release-latest/docs/install/snapshot-build.md similarity index 100% rename from docs/install/snapshot-build.md rename to documentation/release-latest/docs/install/snapshot-build.md diff --git a/docs/readme.md b/documentation/release-latest/docs/readme.md similarity index 100% rename from docs/readme.md rename to documentation/release-latest/docs/readme.md diff --git a/docs/rules/code-styles.md b/documentation/release-latest/docs/rules/code-styles.md similarity index 100% rename from docs/rules/code-styles.md rename to documentation/release-latest/docs/rules/code-styles.md diff --git a/docs/rules/configuration-intellij-idea.md b/documentation/release-latest/docs/rules/configuration-intellij-idea.md similarity index 100% rename from docs/rules/configuration-intellij-idea.md rename to documentation/release-latest/docs/rules/configuration-intellij-idea.md diff --git a/docs/rules/configuration-ktlint.md b/documentation/release-latest/docs/rules/configuration-ktlint.md similarity index 100% rename from docs/rules/configuration-ktlint.md rename to documentation/release-latest/docs/rules/configuration-ktlint.md diff --git a/docs/rules/dependencies.md b/documentation/release-latest/docs/rules/dependencies.md similarity index 100% rename from docs/rules/dependencies.md rename to documentation/release-latest/docs/rules/dependencies.md diff --git a/docs/rules/experimental.md b/documentation/release-latest/docs/rules/experimental.md similarity index 100% rename from docs/rules/experimental.md rename to documentation/release-latest/docs/rules/experimental.md diff --git a/docs/rules/index.md b/documentation/release-latest/docs/rules/index.md similarity index 100% rename from docs/rules/index.md rename to documentation/release-latest/docs/rules/index.md diff --git a/docs/rules/standard.md b/documentation/release-latest/docs/rules/standard.md similarity index 100% rename from docs/rules/standard.md rename to documentation/release-latest/docs/rules/standard.md diff --git a/mkdocs.yml b/documentation/release-latest/mkdocs.yml similarity index 95% rename from mkdocs.yml rename to documentation/release-latest/mkdocs.yml index 27b0cba4e6..ee9f9843b0 100644 --- a/mkdocs.yml +++ b/documentation/release-latest/mkdocs.yml @@ -1,8 +1,16 @@ site_name: Ktlint site_url: https://pinterest.github.io/ktlint/ +site_dir: build/site +docs_dir: docs + +extra: + version: + provider: mike + theme: name: material + custom_dir: overrides favicon: assets/images/favicon.ico palette: # Palette toggle for light mode diff --git a/documentation/release-latest/overrides/main.html b/documentation/release-latest/overrides/main.html new file mode 100644 index 0000000000..033fa3c1f5 --- /dev/null +++ b/documentation/release-latest/overrides/main.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block outdated %} +You're not viewing the latest version. + + Click here to go to latest. + +{% endblock %} diff --git a/run-mkdocs-server.sh b/documentation/release-latest/run-mkdocs-server.sh similarity index 100% rename from run-mkdocs-server.sh rename to documentation/release-latest/run-mkdocs-server.sh diff --git a/documentation/snapshot/docs/api/badge.md b/documentation/snapshot/docs/api/badge.md new file mode 100644 index 0000000000..40b87210b4 --- /dev/null +++ b/documentation/snapshot/docs/api/badge.md @@ -0,0 +1,6 @@ +If you want to display a badge to show that your project is linted and formatted using `'ktlint` than you can add the +[![ktlint](https://img.shields.io/badge/ktlint%20code--style-%E2%9D%A4-FF4081)](https://pinterest.github.io/ktlint/) badge: + +```md title="Ktlint code style badge" +[![ktlint](https://img.shields.io/badge/ktlint%20code--style-%E2%9D%A4-FF4081)](https://pinterest.github.io/ktlint/) +``` diff --git a/documentation/snapshot/docs/api/custom-integration.md b/documentation/snapshot/docs/api/custom-integration.md new file mode 100644 index 0000000000..d6ebe68b3c --- /dev/null +++ b/documentation/snapshot/docs/api/custom-integration.md @@ -0,0 +1,116 @@ +# Custom integration + +!!! warning + This page is based on Ktlint `0.49.x` which has to be released. Most concepts are also applicable for `0.48.x`. + +## Ktlint Rule Engine + +The `Ktlint Rule Engine` is the central entry point for custom integrations with the `Ktlint API`. See [basic API Consumer](https://github.com/pinterest/ktlint/blob/master/ktlint-api-consumer/src/main/kotlin/com/example/ktlint/api/consumer/KtlintApiConsumer.kt) for a basic example on how to invoke the `Ktlint Rule Engine`. This example also explains how the logging of the `Ktlint Rule Engine` can be configured to your needs. + +The `KtLintRuleEngine` instance only needs to be created once for the entire lifetime of your application. Reusing the same instance results in better performance due to caching. + +```kotlin title="Creating the KtLintRuleEngine" +val ktLintRuleEngine = + KtLintRuleEngine( + ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS, + ) +``` + +### Rule provider + +The `KtLintRuleEngine` must be configured with at least one `RuleProvider`. A `RuleProvider` is a lambda which upon request of the `KtLintRuleEngine` provides a new instance of a specific rule. You can either provide any of the standard rules provided by KtLint or with your own custom rules, or with a combination of both. +```kotlin title="Creating a set of RuleProviders" +val KTLINT_API_CONSUMER_RULE_PROVIDERS = + setOf( + // Can provide custom rules + RuleProvider { NoVarRule() }, + // but also reuse rules from KtLint rulesets + RuleProvider { IndentationRule() }, + ) +``` + +### Editor config: defaults & overrides + +When linting and formatting files, the `KtlintRuleEngine` takes the `.editorconfig` file(s) into account which are found on the path to the file. A property which is specified in the `editorConfigOverride` property of the `KtLintRuleEngine` takes precedence above the value of that same property in the `.editorconfig` file. The `editorConfigDefaults` property of the `KtLintRuleEngine` can be used to specify the fallback values for properties in case that property is not defined in the `.editorconfig` file (or in the `editorConfigOverride` property). + +```kotlin title="Specifying the editorConfigOverride" +val ktLintRuleEngine = + KtLintRuleEngine( + ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS, + editorConfigOverride = EditorConfigOverride.from( + INDENT_STYLE_PROPERTY to IndentConfig.IndentStyle.SPACE, + INDENT_SIZE_PROPERTY to 4 + ) + ) +``` + +The `editorConfigOverride` property takes an `EditorConfigProperty` as key. KtLint defines several such properties, but they can also be defined as part of a custom rule. + +The `editorConfigDefaults` property is more cumbersome to define as it is based directly on the data format of the `ec4j` library which is used for parsing the `.editorconfig` file. + +The defaults can be loaded from a path or a directory. If a path to a file is specified, the name of the file does not necessarily have to end with `.editorconfig`. If a path to a directory is specified, the directory should contain a file with name `.editorconfig`. Note that the `propertyTypes` have to be derived from the same collection of rule providers that are specified in the `ruleProviders` property of the `KtLintRuleEngine`. + +```kotlin title="Specifying the editorConfigDefaults using an '.editorconfig' file" +val ktLintRuleEngine = + KtLintRuleEngine( + ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS, + editorConfigDefaults = EditorConfigDefaults.load( + path = Paths.get("/some/path/to/editorconfig/file/or/directory"), + propertyTypes = KTLINT_API_CONSUMER_RULE_PROVIDERS.propertyTypes(), + ) + ) +``` +If you want to include all RuleProviders of the Ktlint project than you can easily retrieve the collection using `StandardRuleSetProvider().getRuleProviders()`. + +The `EditorConfigDefaults` property can also be specified programmatically as is shown below: + +```kotlin title="Specifying the editorConfigDefaults programmatically" +val ktLintRuleEngine = + KtLintRuleEngine( + ruleProviders = KTLINT_API_CONSUMER_RULE_PROVIDERS, + editorConfigDefaults = EditorConfigDefaults( + org.ec4j.core.model.EditorConfig + .builder() + // .. add relevant properties + .build() + ) + ) +``` + +### Lint & format + +Once the `KtLintRuleEngine` has been defined, it is ready to be invoked for each file or code snippet that has to be linted or formatted. The the `lint` and `format` functions take a `Code` instance as parameter. Such an instance can either be created from a file +```kotlin title="Code from file" +val code = Code.fromFile( + File("/some/path/to/file") +) +``` +or a code snippet (set `script` to `true` to handle the snippet as Kotlin script): +```kotlin title="Code from snippet" +val code = Code.fromSnippet( + """ + val code = "some-code" + """.trimIndent() +) +``` + +The `lint` function is invoked with a lambda which is called each time a `LintError` is found and does not return a result. +```kotlin title="Specifying the editorConfigDefaults programmatically" +ktLintRuleEngine + .lint(codeFile) { lintError -> + // handle + } +``` + +The `format` function is invoked with a lambda which is called each time a `LintError` is found and returns the formatted code as result. Note that the `LintError` should be inspected for errors that could not be autocorrected. +```kotlin title="Specifying the editorConfigDefaults programmatically" +val formattedCode = + ktLintRuleEngine + .format(codeFile) { lintError -> + // handle + } +``` + +## Logging + +Ktlint uses the `io.github.microutils:kotlin-logging` which is a `slf4j` wrapper. As API consumer you can choose which logging framework you want to use and configure that framework to your exact needs. The [basic API Consumer](https://github.com/pinterest/ktlint/blob/master/ktlint-api-consumer/src/main/kotlin/com/example/ktlint/api/consumer/KtlintApiConsumer.kt) contains an example with `org.slf4j:slf4j-simple` as logging provider and a customized configuration which shows logging at `DEBUG` level for all classes except one specific class which only displays logging at `WARN` level. diff --git a/documentation/snapshot/docs/api/custom-reporter.md b/documentation/snapshot/docs/api/custom-reporter.md new file mode 100644 index 0000000000..843c558960 --- /dev/null +++ b/documentation/snapshot/docs/api/custom-reporter.md @@ -0,0 +1,18 @@ +## Build a custom reporter +Take a look at [ktlint-cli-reporter-plain](https://github.com/pinterest/ktlint/tree/master/ktlint-cli-reporter-plain). + +In short, all you need to do is to implement a +[ReporterV2](https://github.com/pinterest/ktlint/blob/master/ktlint-cli-reporter-core/src/main/kotlin/com/pinterest/ktlint/cli/reporter/core/api/ReporterV2.kt) and make it available by registering +a custom [ReporterProviderV2](https://github.com/pinterest/ktlint/blob/master/ktlint-cli-reporter-core/src/main/kotlin/com/pinterest/ktlint/cli/reporter/core/api/ReporterProviderV2.kt) using +`META-INF/services/com.pinterest.ktlint.cli.reporter.core.api.ReporterProviderV2`. Pack all of that into a JAR and you're done. + +To load a custom (3rd party) reporter use `ktlint --reporter=name,artifact=/path/to/custom-ktlint-reporter.jar` +(see `ktlint --help` for more). + +## Third party reporters + +Known third-party reporters: + +* [kryanod/ktlint-junit-reporter](https://github.com/kryanod/ktlint-junit-reporter) reports ktlint output as an xml file in JUnit format so that the ktlint report can be made visible on the Merge Request page. +* [musichin/ktlint-github-reporter](https://github.com/musichin/ktlint-github-reporter) uses [GitHub workflow commands](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message) to set error messages for `ktlint` issues. +* [tobi2k/ktlint-gitlab-reporter](https://github.com/tobi2k/ktlint-gitlab-reporter) provides output in JSON format that can be parsed by GitLab automatically. diff --git a/documentation/snapshot/docs/api/custom-rule-set.md b/documentation/snapshot/docs/api/custom-rule-set.md new file mode 100644 index 0000000000..3366e3c248 --- /dev/null +++ b/documentation/snapshot/docs/api/custom-rule-set.md @@ -0,0 +1,73 @@ +!!! Tip + See [Writing your first ktlint rule](https://medium.com/@vanniktech/writing-your-first-ktlint-rule-5a1707f4ca5b) by [Niklas Baudy](https://github.com/vanniktech). + +In a nutshell: a "rule set" is a JAR containing one or more [Rule](https://github.com/pinterest/ktlint/blob/master/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/Rule.kt)s. `ktlint` is relying on the [ServiceLoader](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) to discover all available "RuleSet"s on the classpath. As a ruleset author, all you need to do is to include a `META-INF/services/RuleSetProviderV3` file containing a fully qualified name of your [RuleSetProviderV3](https://github.com/pinterest/ktlint/blob/master/ktlint-cli-core/src/main/kotlin/com/pinterest/ktlint/ruleset/core/api/RuleSetProviderV3.kt) implementation. + +## ktlint-ruleset-template + +A complete sample project (with tests and build files) is included in this repo under the [ktlint-ruleset-template](https://github.com/pinterest/ktlint/tree/master/ktlint-ruleset-template) directory (make sure to check [NoVarRuleTest](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-template/src/test/kotlin/yourpkgname/NoVarRuleTest.kt) as it contains some useful information). + +```shell title="Building the ktlint-ruleset-template" +$ cd ktlint-ruleset-template/ +$ ../gradlew build +``` + +```shell title="Provide code sample that violates rule `custom:no-var" +$ echo 'var v = 0' > test.kt +``` + +```shell title="Running the ktlint-ruleset-template" hl_lines="1 40 43" +$ ktlint -R build/libs/ktlint-ruleset-template.jar --log-level=debug --relative test.kt + +18:13:21.026 [main] DEBUG com.pinterest.ktlint.internal.RuleSetsLoader - JAR ruleset provided with path "/../ktlint/ktlint-ruleset-template/build/libs/ktlint-ruleset-template.jar" +18:13:21.241 [main] DEBUG com.pinterest.ktlint.Main - Discovered reporter with "baseline" id. +18:13:21.241 [main] DEBUG com.pinterest.ktlint.Main - Discovered reporter with "checkstyle" id. +18:13:21.241 [main] DEBUG com.pinterest.ktlint.Main - Discovered reporter with "json" id. +18:13:21.242 [main] DEBUG com.pinterest.ktlint.Main - Discovered reporter with "html" id. +18:13:21.242 [main] DEBUG com.pinterest.ktlint.Main - Discovered reporter with "plain" id. +18:13:21.242 [main] DEBUG com.pinterest.ktlint.Main - Discovered reporter with "sarif" id. +18:13:21.242 [main] DEBUG com.pinterest.ktlint.Main - Initializing "plain" reporter with {verbose=false, color=false, color_name=DARK_GRAY} +[DEBUG] Rule with id 'standard:max-line-length' should run after the rule with id 'trailing-comma'. However, the latter rule is not loaded and is allowed to be ignored. For best results, it is advised load the rule. +[DEBUG] Rules will be executed in order below (unless disabled): + - standard:filename, + - standard:final-newline, + - standard:chain-wrapping, + - standard:colon-spacing, + - standard:comma-spacing, + - standard:comment-spacing, + - standard:curly-spacing, + - standard:dot-spacing, + - standard:import-ordering, + - standard:keyword-spacing, + - standard:modifier-order, + - standard:no-blank-line-before-rbrace, + - standard:no-consecutive-blank-lines, + - standard:no-empty-class-body, + - standard:no-line-break-after-else, + - standard:no-line-break-before-assignment, + - standard:no-multi-spaces, + - standard:no-semi, + - standard:no-trailing-spaces, + - standard:no-unit-return, + - standard:no-unused-imports, + - standard:no-wildcard-imports, + - standard:op-spacing, + - standard:parameter-list-wrapping, + - standard:paren-spacing, + - standard:range-spacing, + - standard:string-template, + - custom:no-var, + - standard:indent, + - standard:max-line-length +`text test.kt:1:1: Unexpected var, use val instead (cannot be auto-corrected)` +18:13:21.893 [main] DEBUG com.pinterest.ktlint.Main - 872ms / 1 file(s) / 1 error(s) +``` + +!!! tip + Multiple custom rule sets can be loaded at the same time. + +## Abstract Syntax Tree (AST) + +While writing/debugging [Rule](https://github.com/pinterest/ktlint/blob/master/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/ruleset/core/api/Rule.kt)s it's often helpful to inspect the Abstract Syntax Tree (AST) of the code snippet that is to be linted / formatted. The [Jetbrain PsiViewer plugin for IntelliJ IDEA](https://github.com/JetBrains/psiviewer) is a convenient tool to inspect code as shown below: + +![Image](../assets/images/psi-viewer.png) diff --git a/documentation/snapshot/docs/api/index.md b/documentation/snapshot/docs/api/index.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/documentation/snapshot/docs/api/overview.md b/documentation/snapshot/docs/api/overview.md new file mode 100644 index 0000000000..333d94a497 --- /dev/null +++ b/documentation/snapshot/docs/api/overview.md @@ -0,0 +1,16 @@ +Ktlint has an open API with which you can integrate. + +The diagram below show the internal module structure of KtLint. + +![Image](../../assets/images/module-dependencies.png) + +The `Ktlint Rule Engine` is by far the most important module. It is responsible for executing the linting and formatting of the source code. The Rule Engine itself does not contain any rules. Rules are provided by API Consumers. + +The `Ktlint CLI` is an API Consumer of the `Ktlint Rule Engine`. Together with the `Ktlint Ruleset Standard` and the `Ktlint Reporter` modules the CLI offers a standalone tool which can easily be run from the commandline. Also, the `Ktlint CLI` can easily be used with custom rulesets and/or reporters. + +The `Ktlint Ruleset Core` module contains the logic which is required by each API Consumer of the `Ktlint Rule Engine`, the `Ktlint Ruleset Standard` and custom rulesets. + +The module `Ktlint Test` provide functionalities like `assertThatRule` which is used to write unit tests in a fluent AssertJ look-a-like style and can also be used for testing of custom rules. + +The `Ktlint logger` module provides functionality for writing log messages. + diff --git a/documentation/snapshot/docs/assets/images/favicon.ico b/documentation/snapshot/docs/assets/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c7ff61748f6e8887fe88cd0dd03a6b539ba4c8e7 GIT binary patch literal 15086 zcmeHOc~n%_8Gk^k)+S`8C~j``zxygmHq-htmVA$U6vdYXo?#hQQ9fjl zboF_%E^_>JSioTchXuZm769PH@WAa7ypnecGjh}LRaqf^%d~zB&i1M^vE5&G14Awx zfzQ4*IJD_26fYYOQ}Bl{ZJ2>l{c0qiKa7A|H}MwB{@5P$APyI!5l_;_(++=wy@Gl1 z7?@_i0qBM_53^qf7X1a-I0I$-)){_g2baX2v)Ho* zlO@cS(xr~n4IB+b&cwA-NeHgGjTc2mS9}k)`;(kBcqRlPYw=&}@QOVx;j=bfZG-KI z9y=D}^2HSVkFtK+Svs&S=h6bWAK8M$P!H73`W@}v3AkJ8CvD32SpG3;IhMXHy^dj& zO>eH_59{B1iO?B34X9ZCCUaOSoI6i|nAk!E`hl2{qMt z+8P~b_x`CnuuCmo2rPv(AvEuR3RJz#IDgUorHV>b}v z1LkyKy7T1>e75FasIn*Q4LZ03VUtjEDHSt`wbhzUzvSg@^N}ZGRjXrzLK9zRUqg@^@qZuYFi5%eVWOSm)3? z<(iz#aAeLq#NM6obBxMdbm^w}Pp8HscHVp4tN)9wrOzDRjDy^-EnYOH zd*PSyyYNpOzu#hg(G`s0de$x0UTt2H`7?Y9s;*zeQ~(2ZEk~O7Yu#x6;rNXYL>96C zoWYsAnD>(R*@vi?rlY;a(szWnA@>wS?5ZDKTZ4i0ArqWw^$ zaaLCAK{;N;>P@aWXdH~(k6zrl6j{R63jZuRS=*MBlk#)9-}=T2CG;~F5L^F7q?X@U zu1EPz3`^dEm^ zKNLva@X0U;PsaXHBW-Wip(H5;C&}ZiWO=fdjr&~lDBDhD8!jTB{L%9_axmiZX?&cQ zj!9QD@qzHHCKH}xem^PKR%L26UJ_R+$^eN=6vb6yy`s2Ctm8c- z#w^Byic%}FyP~i-D<}$!vqF8`Q&Do{H)FlToRzg$C%>H(rDZJ1?Rj)k*i1WmNvFIV ziJRci@M(BWij!XSfX=l<^is{(fmrToxiP%T&~Q)7tarx(j;C8%fPF*!ANDreJ(TCd zdkK$Z?8Qj>TS0qQBA92Hqf2k#G0E#HBy9UW-eGxtPZ)wDK1U|cy{i4*Cf_y`koSoH z-~#!E72InL<(VW$eCIs8yU^!k0!GI8W5@FGC=;2g5AWW%2hx5E7T&8VXW88+wqq>s zf^_JNT98xUd&ob$$usu&C1X*c`T1?_x5W4N=3biIS5@p>B+)OH^Zp*~u?D|o{{!nW zhxZbSv^_yva$Dw$O=Ry8G7-i5*CCL51*^`wubsy)xWC#-9=lV{S_Q|v;V{t_`n(Lh za3?$P4DwgJvvN$nqtt7uQ%8SGTRv~{j?$ON;GS!UISkz24BX<4Wc7|yC(3TX%r>PD z@jNd*{{o(CiUEKt@6SZ~v8QyV-VL(J8L02XAhz8efbAw@<9Rj5*CgKWGDzQTRY$Aa zNwMe`I105XvG`PMz&jN`quo2QWV9eV)zRKzB{P*nnK2 zft=5D&fgL8O|(2)#WEhqM{MI+9 zB%5nyw0sL&$`x6%u5ixT!?8bw_izl-CvAm6WRW|oJsu0sTg7nWJ=hbrlC8=4+7oBU zLy4QN@T_eDdBqA_%mE^U{zNs;ho^~bc>wh0nPmXKLygrKXhX51`rR07dfh3OwgdhQ z2-f=4RAfQ2|n~WN>!hOWt+8KFH6P8ceyJOHW~T6OP9}glgO1kOLe48 za*O2$)+3<0qH*nMih-DVokaX$SB`ggZ^*EpSbsppaJ zdx8GSAllA{Ty7fgRo^1lYSQXROup2_wr+CGi>E)w)A_dfQkLm`^%?e92ob zp|91FD+K z;xCy1g_dw*Xy7Sq%y4r&^^iaaRzHO9%sU>BZ}x%*=d7&^QGB0#Xd|AGygN0-dINao zGK=$V7VnD~wK-GXnU>8wUzT~SWgBeQiSx+YOUEIV`vkN1Xl4GBJp}LBro}A)PnjCA za1S(s=REnEytZP_e+B%1fYE}3d${Mh&py|lb6CN`eP|ZvuEE@&hszw?mduvf}}*e8P3=mN^WP zW+|O@DtHby_4+0BrqAHbJ2~Y|{Y9}4(~7w^70Z5GlP_hMew%jp=Y2MH?&rR*C)c)N zJgW}j{{!se-aL+HWqv%%d^s@~zag*UOFg$q-sv!b0$_0}LHIW-FA-j`94pmRKNoQsTk~9gqelow+h5rLRF(Qrt literal 0 HcmV?d00001 diff --git a/documentation/snapshot/docs/assets/images/module-dependencies.png b/documentation/snapshot/docs/assets/images/module-dependencies.png new file mode 100644 index 0000000000000000000000000000000000000000..029bfbe21f155a6e95365f0da87d9010f1829c0d GIT binary patch literal 119648 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 literal 0 HcmV?d00001 diff --git a/documentation/snapshot/docs/assets/images/psi-viewer.png b/documentation/snapshot/docs/assets/images/psi-viewer.png new file mode 100644 index 0000000000000000000000000000000000000000..88ce1d224c689551e3f1c62b4346e9d210ce94af GIT binary patch literal 79239 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 BG;aU^ literal 0 HcmV?d00001 diff --git a/documentation/snapshot/docs/assets/images/rule-dependencies.png b/documentation/snapshot/docs/assets/images/rule-dependencies.png new file mode 100644 index 0000000000000000000000000000000000000000..f13326a160399e6850d8f513f10d13d88b82944a GIT binary patch literal 43730 zcmeFZc|6qb_dl$JQbQZnNVHLwv<$KiZH>~uYGc8yFQP*mcr`6JwUF_nYN;(MkhG(tEV=q|W?i?N{a|JHwUT9#OTQNi zAFRF|XiL?ReoJKFAcNwCv%BLtE7qO6;uZg@pl-C+8podXjz7hWPTe!utW)1=5Z7(V z$;@k_nPHs?0Yv@&~kAWa$0MA8gnH1^3|DoY58|NRCXG(~75 zi}}mMQ-}O7Th{ved#!!_aMu>e?qsx6m=v=zN@EEvVC*4#M`Rq^6A$^; zMKEG7_@l4$7m|bqC8AqXQ5VKm71)!ErqXNQnYI$aG{d&_rlh8gHQWPo_yTzu2IZBD ztvwVM!f54`5+Cda3i6e-{Z#wLe+b7Fij~Q4SEJE_O}Ku=HEMW zbqAyrx)^~|P1>C1)ETiGHWpE-0Z}ISMp7CoVZ5Q}t>sI&Jlb#_6L_yU%0PJ}jGy36 z(xc*HofP;wz$zivj)P@NL-`3F=oBT(*SE9sF(8joJG?pwsxg^G4m&BvLQKHrFi>8s-r- zn!n)mBBkg(Y17$eUCqAU7n@utjjik+dhkMdaa2Kp5okUIfg-xX*JTOPfNJFrm58go z=#qwAYc!O(TAEiK=dWU$b2jyFCB$&d;Ok~)u|`O-mu;D4GMt5J-*fW z^cr<0r2m8>L-|j7=FjcRaJ@o7eC$1$-JQv(PwU2YZ<6m&mXZ{N({70tj=Vk=Cbe!; z?<}oj1K9NX1mN|3h>qf1etL$MR=%gKOOAymDc75q97N}bz?KpVuute&k&bigcRLQA z`NL3{Tc0>qyL}hxSVnj(j`MzKJrX&!|2X77K@6;T0 zAv{M3!yBi@ouGwN9!05><{{nUUnV7(YR0vFlCijBV7ICQK#6}T@1cLi7c`o?P8AIm z%OlRulvU1}ZHS4(a$17pK0!Z}OvcfPj2+CFrEc>dzsHyzM1t`Xe@h^OP|*ox$*~uE zu0n|FhUxmLRGCGhq7SR8V2bYIkl#X&e!l-QJ%gpM15;3eZ+|cMY4f64_?TA$~NU4&SXU+ zYx90pZ64bbB?t|wbk;>`DDEkEnw`FHF_!I#W}UZC)*A<>5?&eXdjY$`)-&&z-4I_r zs+y`~Gr(+SrH(WZPkt4Ks%xwk_zZ<@-%)dH;Cs2_CDB5;qHMR?<;OFL)$8=Ia%@5?R~zQM;3Kq13|cpZhCsX&(vJO=>h|gfTT%k0I9rW4@Nu5S~}! z95_8I77Dhtadq^jS1{Y@)%nH>nyRICSU?Qt*F>jR&2|@eB!*<6lfDu-ePkRkB4qU{ z9`nqCYzLVcqOA1qd=cgAXL&Mf=EdG+en`#3F5y8I8LeSzw-{5Va02nV)!U;^ET04f zuTwjn^o4rAkUy2_V;-f3X1%Y|=H`edqd*YUa91>Zg3#|^r~UjQWE<= zB`*d<7$%=pAgH`3$iBCY&F;TmNoFfL5FXF(s&APg2Cx;`L?@1o-C$rojbTzG&nheUbHponww4(lGd@Wg3#?1lbQ`DL)d0KD;#z5olNJy@e=jv4GjyAi z;;=`}ux+(bqpjeXH`I_>#_vXnpm?m!Ez zk`5#Gj@cb5k#1c)dLUqTSY|Db`8BsJj6P+LV*1pKvLIvL6wdFKLenTpOs4G4iK2`a zoErg-(@8lQMZx-xVVbTV&Eq?NpI{iDkI}AgEm9GAq+weo7SM1#P<4$0>^!Dy9}ICE zb#iF@9VR?8Gxy$a_=0bg55AGwAfK4g7|)Dd7TE_g3e&YEN2xVfBR7xl%et-2i|wfC z?3?gP@L-lITfiUY@Rxk34RS*gRZ)8WX^)t#lM=B~#^v-;QgB!AKok3l?T~y-X0^90 zg2-lnjfZc1^%fzPIrLP_QT2R;dg*m4GLPy>xIb3K(N#LiqZn=3W3IObgO!pe{Yi4a zdVN^|gw|@(13>RT0#c>T%hBJp7<*qIFC*`S^lD0^sMP6!n7> ze+V(TtG2chU3NC(tKbG}pS6r?K8)3U%B*!w-HAqjbnT>BB-#K4gBJHhdnhB$?{9UXIEw#Ej=9Iq&H`YiYZMff|3=G!vz|G+17h`DanAte zaFyNq-_*@Gib&jgzWL}r*qNT%MYpu}LrH3lZdY2uO%W%wt?l&ijj8ivWbIKOK`2R{hqTG;NIDVMzSvBg~%^Gu(M_qo@L|(mLT)r+l7}w`hzs7vLPWbUy z?3du{HOhDO>G$RKdJ-=jDqK$~<;jac%l6QxwjV*DX+~-d0QBsz=3`VfY5EPM*XE`=V?Ei zvx_D-mX=%sW%zTJzn*RKD&fzH?&%#RJQGqBjwl)ruM&?4&s(EB4-^{R61Mt#@X+_APn4dDpbb7rj`X}BG0 zAGCoqM1BzauIj&TrD8ihvpLZjNxx#qcBnrz=k~l-`vh(&pVC_?>yE#BDtbWN{Zs1p z#f$XFp1r5)KYY>euIZlBKta&GvRVxBC|vu1-H}}C<$h~xutj{smVX8f|E0uUDeq0& zMS56zsQ#Lv!|yS5cf6KibSwID-sdvju-O1$un`iKw~DUz8~4>+s+v}@cPAjlvukf@ z3u2GwO7}psFy=xvMbrau#00;Zlzv)5Lqk{mY;rX**sWK0mcl3>%T1G$S9r;Hv9wCZ ze3YcyyzhR|bpM|{{=v4qgF6rEH)8KMSNhb|K;NxSS&tG#Hne#)f(J+WRV5bv_p!Pb zHA%qRMt(q@)E~-T&SSZ7DsRm$2GEthj_o>M0ipls@pRdKg~5EWW%368oM1eW@>jSS zW4eq$SiH=X&>XP0%N_|f>t3qWu{`({U++(w&s!CPq8{P%BPi-f-JK z%CFR`xD+^Cw;tl@8HhBQ6o6d&Wih5AdLMRVZot~VV?A%oiGCC(b~q;sOU;`Y0x{P= z*RNP?iehYEyk+_h-GDT0`uTx8H=- z}QaYwnUw#fLmXs_L(&55UOxEd$^j? z!tr})W72C`!>ymsaXJRw^otbz(IZ*ohzY~DD*fDJqYk0VKT3z{wPxP9#Pa1zuO*P) zvr%p4%Ij8A^e)$G6v7%b*V1dpV#+?H#P@!kvLdHrH5GR6;skoo%+ zJRp^4<`D?-AM`$ACjNc9biAGW5hi#ajQg91=QcO|2QRII+F(BL+Iw~DAs(J9|Emsp z^_&~uZgzH~QXh>}U|p>0-BB#b>c{kJUeig`c4sF6{=NlVkD~l{IP@SA1@;u84!Ci6 z-?DkWo>-%pzxpGd*4{Zq*QM;)%1Ww+g#iWkL{VRiK@bkx%+P)_EdU<*9){T#vTvU& zcjP}DIjjki574=;Flrd=g<^<keB;Dx2Z0=gsZ}l z(WV^{?OdiF1`i7Q&y8x)M#ZR7a3&d-9W{z$MW78(%h{f7FW4{1>LvV-<>Y?@r*8`Q z`cm8wG;^K9T=#4%&JU>pSwCHDcTCwA5=vt9*x?VBoyrZXuvwZRIkWM5Rcs#(W!FOizvn&YMpJd(D= zPBn_49oXBJ&>2;oc2&-X^hw(Xopftt&wWU^2^o;)m~RM! z_WEWs5@jGp%uF_i_!AjFjajp?XqNp`5&x8FQjmf^E@;wVL&Xn8J4yk00p~>8CU9|P z{e1Uq1TD2XvP-yOdN?@IFS$u|<2DmnvoCPa!oeu`AWwAWdj1mYi#0W_Pm@@FQr}p3 zyI!7>x_19-vys`|34Psy7cDgDZ~qX)IY0AZodNqz9#Puquuf{hMCqiN1Dkt8(}blF zucs~I?9-(6)6BavuzESV&uige%|=~3k5+SzHEVHvvMrjW+b7L-sT!{egxiX4u?8BC zQLhiSH~mCus}7ePns(6+%WiEjEh(hmEDmYm20p=pMUzU)=7omKIogZvdWNekhgT(- zriijb0v+K8VBfv-=bol_LrX7^TUnI2DEk!YyI|drbfCRPmlbU|+%$^V=Z2sBkomR% zkirp}#jVAVvZI`sHu8uGoQAgdi8EuN{7Mpk{YaBG$gL@m6NMQS?L-eh4&?_TEY)eX zSB_9!ZYK<8)!GA$#Iv$pf^ZO&r_4~FaGv>~crzi&2uye}?Wjs{;R)L)r;_gGUC^Pv zKyhq0^p7R6{l0eCK$0JSha3WvzT3Sfr3`FRz10PVe%htW_+>g9w0C51q@XrzlAF@o zTA_wc$2vQhXh7ujqdXPeWkdPlzUhQL?0ALv-(`10Qj@Jm5+V$YN znPF=4y5{GJ;mAEld0hSwDS~B{ahXpJ4r8_<;&vU8p$E2j1F9zQyc#J25@9(z0Esc>64B}(ff-|H=vJ8#w8c?WVjrV;PTjba!}m0i2pM* ze*?YVxlOpikmQGfX~=iqeA%-)MmQ~V9z#&CL9V>B)#eKnPw(v#+LN_ThhA4M;!~7E zNPgQ*xv~tSN1(Ct=H0)c?9&WZ93*TMfAWFJHm0|5xuCE#jPA^@R_0cTHd0y&Hy% zYl7v@_X4Yr(0i#oe?2wnkEaX0vc8G-)9TkFw431)k!$3wwgFzdIki4*@CqA0O2HkT z$|H1*53LG>27&)JBDl(Htr9%zg2yN)6pjhe2y-i$O?PV_PHdz|a@G5MB2Y8D*?4lL zB;rTX4R7-u>#nA5Ok2K365zjx)8KL~k2)f&v;%@n`+edMp~KZIdO=~V>XHRloemdp z3EZ7HkN0SFS`T70-DRcc&ki4YB;ApvAjC)Eo<+t!Kw^d+^+lO>bMP4fluawiAbvb{ zI+Px9r(+e!ONbe`o*Hwo(=+DU8Np{Xm@$0BHX6C7uW3VP*sl3ML$s`EVoPk=PnVT{ zKGNdu&BZ69q38d`irk*4jcXoTwDWOk&%CBQCsrQ}=)j0@g(4n0mgDjNtqF)n>Vk0} zwK=p;i;~@sqZZ~0hg#{iq#a1G>5!giaA#C{Z_bk=`B{H|&R=hXZ8yTE@+5hx+ z?)kj(*HIMPWOdJR1Frh7M23j;xTyLRqUz z8&|;s<-33{bFIFiEFmnMsa6($ia4uz=RrefJdv<(RV)Vz;xlKC@Ns!+oemQ0#V*&NYb0TtcSzi zD?X3aA8xK(oh_lxFX%}-)%i`Min*-8yX=JC?XL~y)IK1NXSDr-Xw1e=?<$Lza;U8d zyCyb5bd~7R7ef0UuhL9h_yPQ|6d7%Z38O`K`7L7_ofuT=VoaMI)PSdUH%%M`yu={6 z^Qn4m+`ouksC@7v@KRW~fNIAp5lU=_@**GM`}OL8Y1Bp6t7~R_{nO2SX{Ui|_Njp< z@9x+OYs^`ht<=zN-k#$4FLYw81M5&2CBt-PNVJfcT6g7c-d+!V`}ap@)WnjV z0R1o_ZQfU^y9eENLk}Q4pzas%4ZihRd$FA_54)fbl$q~htHZ#BZu>IDbwp31 z&)S!c*u#GrE6`Q{bYB2dK!HztEe;o?4o7Hxe;NBQa}tx_KKj8RUimT0p!gi#W$`#> zmxAI4vUHl8!FX!JnJPl(NQyw{@-CnXzxDy5FSyg@F+U|cl&Lq*ktDD4lxK+72e-LO zp!Sb4yf8)Z??B)~?sG?;M>c)NG-2!nq`U3GNh9E8=BQ)uwtXa+ZyZ!S1+0zuot%GLrL8d_wX3ZHmmsYZ;MKTc5{hwHb?73rg-s4SkVh>%{nD=?3M;^ z4I2Cfi8~-^U|FU5ofmUy0gjTIaXbHnN%?1*2r$hcSMO^jJTjOULf+!uqm-0OpxjPK!t}6;OXaHDepl|ActCm2; zO?0?H9A=5TEY}f)fFV2~E!<_mZ2`h!ZqNti?)`tAhhq=r%P8|p{jk|DCx9OoTA`co zfcUMh@^KCEdG#F-eCbNRQPXxl;U}Rx8~w?@$36v;TVQvK?~YYY^;Qu7!zN%Q{1n~- zoH*qE7E{3E|8qfZh*Yg76?L&B&~iiO2HG-S_;=AwLQf6*BA9^(aWIr$>wsmW7B$XX zve7>EDzZZ8DPSrOPbaWRi2cfU&zA%`S1;kpunLxaWeN+(^OdfCH#z1bg<;akz6tv; zbI0OMc0fX;p7ZT7u&=Q)$h(Gom(a*Hlpzm+-G*7n-7O9vj!RxXe~_*&`I!3v<^!-Y z*Fknc%1NN`G#AQ~YsMc_qEUnoA12yL>sk89oD3BAmwu~()%P^-(gT(OHngU;bk zDcD#53YvV9-2Zx(RbxX=DV~-uLzmvI5_4_i^2Q1}g&h)d!LjufD-^8CDoS$j2x@k`ePjo^l zX{hl7Qwm>7eXTp5hH@d^Bg=l~GzMqGVq^UfXNl?6a7{wjuEe`PGCx8~v|~EEcE&d3 z4Gc}*SV^@n_4B^E;lYNT(?>HhR77Syg3q7SCk0NK6c@b7YK(|pi1W|p|1WaxX@>>t z8uE8s>%9(pE^-)O{W#Ekw1MMB>lt-y3r|%D-8|`bs_S;ass#MN4xXTRf+54%n!Wn; z;bpH4o7vgTzt56kaS+i5n_NKU4TpqcM8H^Gz^)Lv%1t?mm_R(hPi$od><5##(P7V* z=M`krWEiT#Bh<$-8~her zTQmh+uyb_v#9z^SL`Vq%I{I)^mL47 zYMrv1UG>M$?UklqE9w=k{Y)RBTN|3m3y9J$=AG4ly=oa?gTO2Zp~Y$vu^$2!GNKgM(1 zs!?h78q0Uq4-XLkGBg{T{4p7}o)_AJ>!xkYYPxlhH1TpWR3Eray{|G$-FYx{W%<%% zOEw1uV7?bWqtP4aZW<^d@?VrfIy_;K3igo(bA+n>`t?n|LMXRY~ zfjBrTP2>T-ZuiZ;@bd@u_WzSGASL1O(Z$ zW$j{d_sHqH;renz^-oMUkj(KgvTxp)he93Qu0}i<1@h_6M>2zAv$|!AsPXq=)Y-SL zT4u9aS2Id)lRvVH+^j4K=p~oLtWtO-EC1p}4;2t+;e~*Cl0)pxXfLAmLt#dlgHU4p zYFGcFmuBAU5o0-xrqqjpo*M@{XkTl_nNhyX{F_)1z-VU9=B8#^KC>_+tysp{>v^Zq z(!>wvKMD2liJqUmKY#h)<0TxnGOyeMe>Z12qYyVIemCvgc3F4q#E{BoMMN=*nM%_v z$c{jni(|#Zq6Y%5tpKxIf=jPk>e-aMK4JX0(cW+q^R$vYy)E(Eg7#z9Dq)r*g6O|E zW?18~g6113|FLk^XaQC0$F!@_=*SDag&bfu_U6~!F>304|pJz)WG;n5bo^`2w0SO zDLW{+Dn?l?uLlYC$e_=SpJ|WM3TT0#7)C{(r0*=@qmXZ59)~pjo1^OHK0+Q`xR_tk zUNTwS?an}#oU2<^>0bBDy^wCMF}F!H62dxNnLj!Jh5d;*nk-TGA&YW0bJcZTwZ!#A zk$Uq1+WjZnuf6RI{215+?SKk>ax@dAr_{e5x1O7$7AE=Ps=I6(%o<20?m3?B9qo=o z=1v{XqE(NVo#8TvMi;Csoiv_-Dwof3Z;%}-;Lh^X!Z*HgG6=H@;~#vlG`Mqsvv4-C z<=Y0+^BeRldvzeiJPn}zs#4frk3@dGgeyIlFJM?)Uc7uw#M`tz&U|duq4l2O=BA27 zznY{dEXNOZWtx~e&y3sy%@HfDEktPYq0@R(AI`Osc{my+S^_w)t{Lx5sKmgo= zddBsx>`)GR;YEF04e7ynq%ZmSu0jjE)>A%z+f3)3p|uimYVOD4_}a764K_fk%c$Vh z59=;)TbA>N@<&^R@+aowdsnX5#Z3eH6CW6*DVC(+B+T=*=RNFWpUsx3WtkuRQC#@t zZT4R_QBjVoX;s6XIr|H`6DRPthR*aIkonvx>5joqxR+9j{hRzZJH%j*Ta8^$=w*_3 zK@rzOX>-n>S1I0%wAQZi2F#MZ!j{>(rL>-273B-LTI}O#Pn5A_Bq>f**pQ#6u23{P z=A1F9d&t4#Oey{4Kn#dj|J<#22_qJzW@hd{u5e^~T4boF zJeqHftn1AWwROce+7Yr+`73Rvf23Xk;1}oUaI(;#*6X&hm%Zic+Pn-#&r#v{LJs5d z)8|$PFI;(=z6-%8*fMKJqh2UEe&wU`!K#kZMXZ}WyC0LbdPAi%z~LJ}D#OMCl!xjB zD?6e zz-wcJoDKLgXFGZP9&*c%wMG`e5xia}IMVcW=>Xu?9w+8Ny2v;$+|U@&fy=q$82ZB>~+b3ER)e#_w(JM{3EI1*M|Z&9+9~vRBgupC3QlIr%Y$n9&i4P%{{E) zc&D*Z=7me~=U+L3h2)ZK0~EoA>@BU?-}lHAQPAs3^d=c$qyt2A-h*m^->U;6Goa%jsRGcoC+o1g7fVpM{EdVP}> zi_jBs)t5vRiHb#ri&P=qXxfUatU6@Q&qC^yVUXwnHC@4Th%8^_??$r05^g#gs+*1# ziM0WGd-r^I9ZxK1S`G30$TZiE$PesX*oE;jMvf7Rpjh3o_Ggz474ATWXUuqkT1AZM zKA2Xz$(uoAWeqyP7*&5ys~KYQfsIGrB;-E2__f4#tnIf(#9L*}M2Q{zLG2lJR=Q~) zcI-=$Qwoxlay@)y-MEg#0zzmQ)|wnVBxMI2E08-Fx3FaF4<^>1cg-g!Z=G zGG{uw&8?J@kPv){)p3Lt_YzbEi4NG`HFI;2-HCsUVxO;fsP%kZ%yiDMZ$7Z;_!~qp zXW?T*`0%#fPr|mV#XB|v#qz0nP z#M5Gar@HmmE>~1Kc@!VjnpRZ`w4<$a7)kwYj%oZI4l)GGBXa(?*?^RO5S=F8ck-~W z{^A}@7v{$Bsb$uI?NMlNGQ$3K(YWs!G#EAq!gPrLM(W=koKGCk(-TYwK za(jg&x`1NDosAm;!$y<(M0dpb_9^;beJ!CiJ+w5sTf3PwkEBQa_i_FCB4VMDH6F7q zDNO-9Bc!K=!lBo2)S={c5z2}IG$3UXUa+;P^Fbi8JntH?$|tfSm(+0iV7@RW*!kCA zxRNyT${c-GHQU9)X)@wcsF&1kG6=b~KuQbooST4+TsIO*zAW}gK#N&j&bf>KF46u1 zx}E>wuPXol3_c1-{+gg`C=f?We?&sd4GD#;dfy|YVxGd^ohjD+ujt z|9;r}^RCWU*S%?*16#SUzqm&a(nnvDNh93pI9;`%jsv(;eF=qmXV4%+ePjvW+`8H_D#m~>?7Meb=tF>taYX-@jx4gYPrwMEpjZRzrQf((=+cw%k+zRNCX z{&*$!&by$$ABET>|#v51I>G}sdAZSLyNd=GfcYCRprt5>*|E(hJTKUu55gXEXpzEtjPIhEO?oujAG-vSuC$Se zG?RB%yA9vR1Akp)3e=$S0Ft?wPwTY&PAJo>fMfI8{|sAclWZxJqAKxDx73l-BdGv5 zoDH&hSxd4rY%iXL*>Po3rpJvLma91)pRl`jbq8hYr$?*3U1wH(MA*d}S@PCt^O`}C zgbN^HvH1^a$$+%-2sJGAw7FrjK^Ju>CE2R-;xK(vPn(yNY=^yI0arj zb03!a4xI*eQmQt<@KkHJok8JPwM0I!i1=Mxo3nYd8h7-#zNkN~^%~*lk+QeE2iS@8 z&`V0g8@1VQyU}!OjPKP@-iC$l%?s5*f3837Bzz%d;nZU;@e+{&wf9>?e5>N z7Um43epn?o$oT!-SZJF|4dxd5Yk%P9i2_%j{-k*$i~MA!z2v|wa}K-7h}CZ1d3ZtS z)`ny?l5F*OjL=Rh#HknM`7cUeI_Y5w(6QBDpDf$H_!475E}4%E|J3AK>>~09%3Q~X^L_F_|ddb_Q z2)ALQSY*1Y@{>!J^==*)(Vc@+8CP9&0gF+PrLw;Oj&GtpApmYVa8` zlS~;F`OmmAY|+Oqde7SmKdx`w#;`l!U|2JpPa=SoT z2fA(rjYej#(W0;&QOULrE&LtZrpgxDvO5@W*;ECA5QEjGO~Vd);jT*wJ36*WDsUqL zfPJ@B2|vuz~PSas1DD$1;+Y6JaEe`$5TI`|K2BVx~!5=lG2bDoQIzkX@`Gi9j0+y*%|OiTd97YUjwVnju?OpdG@ zq3J+Zjb85B_L=MN(m*Z1E8xx?|0YwHtzavkZJMFpuJ`Sbmh?x^thiMKU?P(Dr*OPd zIcHSps$o|ScJAn`bWEA+WC_t_(kI=~E2Y_(Gm)NqOUZ z*BG^(XJngpSeiPWX8#tqKHQyWRedk8*iM6+efJ6c95lDwR+iA)O+~@7lUsXRqSKD% z+rM)Kx4y&pq6`|hRfd>)csZaqaNguKR{V!W)y4H%d-Xd@!MQ(0T+y&IrSle`3Gi|7j2&U!rp(d&HE!oiO{V%-cb08nm}8Wvtbd|s6bdA< z*E`c%_@UVhBZK;}N(fjY4zUEhHlH0JHf+2{fXu_oMY8>(huz-#;v5}L(;JZj%jF*p(Szl+@s>I>t&@L+$zY6JQc?g ze`2|vW9((L-E|g`$~G!IY?U|<`V}pTj+rgNLEZbt`p}2v*=ej)x7@T!5Bh?BAbBje zY2)f`oxX`bMqoF~~E z_X1K^#Wjvh`L7eaj+HQt9cy~~by|B5n&3;$1}`tNM2JQjS7+%vZi{t>@^b+^IbI{N zQ2u=&n`W^40~fO?&fj-mgmaDPJ@cgcFW>c+=A2bA+rHivc-@9UaTg*pr;1fV#4le} zVg1Qvd!6oOO}^LdnO5+?H$t7QziPjIl@X{ILT}zOdP=wa7bNo#cqd+A z&n(E_=NQ2UIr!=fb$VR6O<-(y$MgXDt+GvlC=3Ckbf%|T2})XQ{5;^)%kx*o&(olN zzzAh_hj(c4c#p^A^z0Q-p4bnBql=kaXHmU$Pgq5kK`sz}_^!{??Cb)eI~r06!PM`C zJ zs=QNo@RTn~TkNacT)^_SN3T_+`AU5Ix~kE?B;-GquO4+q?LGhnHCUj%_RYDDxesrB zlM9Db4$B~3JJs$dR+`j4Tz;q4P}dq8M{OQ{*%xeJU4Fts>7BZESU$^4XWjA!#4yE) zKHh`lHer6B>`dwknZMkm5AN~W46TO^dvkv&QGUL6`gwdw1%BOGx4Zl zoIh)+XK7YAx+aOmSe}%r%%sO8Ynqq1{{a2Uz4V0_NvpGP+n3@=EwQOMFOBsF!--yw z7Gqd{@yljIHQ8N|PqvkA8sZs)rVR6rJ^*9Y+?ssLqE-Sp$mavgvwT_8b8eotrqSau zqtw5gimM}B0gZnwepO|5gAJQr2AVF(-k+8t7mc??#Sj$lclf%s%1BH6uop403;r)` zn7GYR_jG%7@%(o|=<|R*&cmRYslyq$Xy}G>{k=(}XGqU4szXaKPumk;^l9Gc;FUj6 zP^9&Od={PU?A@y(`%ahetVLkD`e9jn%8;VWNFnVEZqQig>w<5(LtjAOZ+Qh_zFt38 zHFPDg8mF&V6K=wu<7_OrS%fuYuafPGKqco5svEHgr9fYyVUOsnufK-g^Ru1V99dsk zwnQoiwmFe0Cb)z`3B?s35^=AxV-Oyr)d@r^&vOGYj~(i2B>3G_#I*k3l^Toc#r- z7pzKmNu_R&;~Po$niG9c2mdx1{#-iPDc~ar42h2E=0eQ7nO*M$WlS~cy0?bZCQ1IZ&W%tpgmXYX!1jZ)0UJ4)tqd_;#`OF|#E+)G%yz1z&5n=57`EZ>|b$`7P zWyIg4#=80s?*T5h+Md@97n(F&R0IVeud~IC{M4@u?^93%(AlG|{ia{(DpZzpJ$xlN zuC=RnyO`Cn-O!=B?Hdg)HrB%*-H2BnvHeT0MYZhuFWIQ^HOw`Ss02NmGRuo~`(&2q zE%JlPy90hXug^E^9TQVc&8G}Bgt3cS(mE~ELt+DBlR2|?eV5%cDOIo?t>KZgqfFwf z^v$5_Q5RCw6h~Kv7d6ly;r^wjDIP3?k6Le~%F8KQ5X;)%-?pPy502spD`GW<9eK9q z?v~CR|A@Qfdfl>i8SEYuy*`)hfUj^O_yBzglz+MCUyx1BV}9S>AQMxePGKM1Ei_|8 z+`A)KS#g7o-u-n*$zxQWj}gw5h+?`gN^UKQz50fQ5K~3TUb1^asi#%WV@<2Yh^Rr2 zblMjdlN>)Sm~X9#i3P^@yY&)Q@WaQ_!TK9L=2+QSK{N7z#AQ7(_iv<@L6<@6K$-i7 z$8hR4Q0bgdJTx{yFaw7D<-vy$dlOaSV9H1Aofm%p@eeXu-;ig1^UR$*7?wEmcyOS) z@;SCddjG0gPExLB?K00{O1M)T4@3Bksp!Iu=j^JH0AzRCRJ9b1A7Ya0n|AFmOUe&& zeSYJGuTJ@m(3O?1z4;%1c0Xc+y1=lTg@$p~{^FD70TuFaLp99&ljo%;?fgu3Z}k_~ z{6H1oZWY1=9YXsGAn=iz;5VsjW>z`H?xE6kIw7C&4aSjpjz&)FLo=6 z$@+h`%@_>3+Ug#qtXIY#5*TGQ0{XN&i2Lb-#SY4#Xg+~@h zsW%w_)_$WLPFp_Cm+Ld`Q%@;lnW1=E+V@hTGGQtHw`-t*t5Qd7I|kSC0QOZ|_;pc0 zG1V7>fX6FzYsd_=%P>5!mA2GBJ2+N@u|5UUiLm zatiJhF8^#G=RUvK+!e=EIuciQvyb{~QFpo{fOnQra?z$E&I~}U3)rgJ749!S|DXuRry89t5)`vmN{++{BK$YwShtIxmy5hc`CEzPi->SQ zX~u1>hdmNz1(fJReSNA0dDMH(1JRu+6@xN8?uTlA=DYR}4ay4m9?K8RyZVXVYLiBI zgj&;Ww&3fjkr_c^r6pTFF0Hnnqpo|K)^gACN-L%%xg})IImGncI}&tP2}RmWrMUe$ z)K66j2u}J*WDViB;z%vwVLJB|Grn%=tIrRsEO+Wg@-(|#dS$MC!K3>DALNQifa0}s z+q{F;{%EFwC7~?Wud}6HoC7u0xt2yh)i+kCC{>Vtmyn|t^LL?6=Gpgi5(&^{mzkWU zhla8&IxE}^G(t$e?(J1hzl;lGX_hR{Wo*jz)PD{%o9+xx%~9`jVu`xj$x=rNyI$JD z=R|&G5sj$?cnnlDgGse0cK0xfoz)H z0g!$UB7m>4FcU0qs24oDs&PM#H}IMLD|id6`-&a1qWOy5g?Hk@xBcF&IqFtmnm(F} z^*JiFD0j1*b)e;iI1QjgGcQ0VRFRd+eVScaiB~JB@OJ{<$+yjqkuDu|(>zQ!%>lx5 zJCRs%`DIEVqb&yw0f@9;kR`3ID~eWj1!A6Z$yQmg{m5=qEyO{Vnm-*~GLaF`QZ*|m zbhdsRqn%2*NvP}$t48=M-bz5NAHrN^mN$gLyv!BEF%lIqEMs))w9Tl(rLt9if1Umj@5B-16VL-B=@YtF#b)ug-JN=h;u^~A|kv7Q%QO>mPd zno?1EVLBdBTi9!o+bUyDO*PWA89zXn5sbu*a>KmN8-Vvz3M;amN*RV38 z6dWBKV?oxU-Y`$B>7Z2EE=uSP4-*fW*ikaw)=BB$k}~q!W6CvMh>Pds%iM{;lcA(c zaNI#jMI%br^ows{xqi~~3x;?}D%VVHF39j?b|-3ejYV{-rAudR9ChV+-n-di`wGSD znml5BjRTDYF8bZuiQ6Btsgi#6Ccp{(crCTdQIwHeVLyJvwa$6x+az~Jq6F+QTCbKr zQV=>-t@{nvb;`_4Gber-@gJLj|qc^K6(BI?BSq_8p=3TZ+P09M%@uu_suV z;SoKwcNgW&U8bjW`9b}~&ps4+5vH5NbQ?z=T#+|UW-=_CkAd)H29#Z%m@8PztF0O| z_P?#y7wFE~-ow$!fuGyRzwubkFn=OyfuJx0vo6+@O^foV2py6CyC5$Cn(1!tocGCc zWl)Ol$;lbEP)6?=F=~~vjAH~0R$Q;rpF8<^FT(mJHgdGOL%3D7=2AnRUOsEvfCO*m zx31bX5l-E-0#z{`9(1$sf@NZKXTea8Y&KLeacM|H$rM$vyIlTdu&{5vNurvjb=e)@ ziV5edv-kAexpIiuGg{$y=AHOEah*7EOVB)oFl3f_E`wvq`~Eqw0m9dF*j8EVZvFMq zd!kCp&zOGOUGbpsWUBt5134OJ>-v2*xyBI#eb(;e(Eg7iR2e5Yjme@s4hK#?3+!AY zyzVqz0^b!thN)>TrK#5(sDfnnbr?dPl`Q8l+uZKviB)ygqJ(RsS+uUICW(SPnCxz# zL89}Xf$UzTe7`K5 zwS0zMztC>|a3=6fr(3Z#l^>$)#mKnWEah%$`#r#RV;KX=!iq@);y0b1g1q{F9ha=v zXL^4~U!Uw%^@97B1;2_Sv8L~(Sg;Pif6wbH6tmi@%fWsbuqBLNDl81xz(tu32CPO5 z)%@*jdo{5D-(OPYkhpv-$sQ@OSfg}1{$uxs!+nT|XBvB`N(J~Sb@T@$8SzygyZh6& zn~}IF6*RJFSCWse18hM!v(v5!eDA`2lJ?3$b9B`_{mD%|u|T*Y&wWy}jAMKDjox=z z>5x?cE5mi4bC0B4IY+Y0b#d{j^lLDw#XmWGu!<2*J?n?w0k-4~1@4oP(D(ZWR} z)i7zv-Z6Z<|E~6xr0YMVs{5-@roC}lgb%|iJ+Ed4-@gW7TOHw1{^$!gbDfLtvM2_V z@$z>u-tNfzTPCfltPOvq>EJ>u^gt>25*jZ%4%ADml3N(H{vy8t#675BxNV=G_*#8w zsZYSV^V19H!;vL{csBnAIi=&_(jkVM00<{b0-MJ7-^W>k$ghr(gHE}TpI;hiIsH9! z2Pj>`7*2oBHYaW{jV8Xei}+R$SecaJLbu(BuZxBbhKz4ieFt1zJ49K-&m#%gyA(rt zV--ijQ%z#UUFJR(dn7)^pY>4mNhGThiQ4Ud=PDWNu>aqaeYE{3j*@iyXL@@=TA^T_ z^-}4Vs~njxF6;TUdB54jjkex$Et7kN-Gi*Ih5e$$BGd9w|JSl}pLdoB-qEDR~qt) z8yOR3NA=?R3}vL6xt9_AwL4z2n`$>zdyd={>vfBy4StdwXSuY=XGA|W`@nDKXDgR zHP73DkNxOA>HV+GedP|~UPsG*<9#s0UKqu{#l z7CLgG@=yqlpnLX^T0nBC*ow1vVS2S!wV%Lo3|nRvap_$o1CXTX5n z?cV9eN-^i=S^9Y*nwR{vl_9n&n8*P0o29L7YdkLZ4Pgx7`%WNcF{DitCm`4>LQ`CCFsxV?+?NB)@(`y zh~~eXXAeqTo95?|p~|KdbFlb8adv}PZAZZRfx1VtZORB{LdKb7Vx6*GMMvePD} zca&*beVSbFvYdG9pq}(#p-c@`W4KpmilD*XQ zz`|(y#Y2T(&9g?4TPL+9E8e5T@dlAo=hu^uMztx(dTvWwMVfH2jO$sMKbe1;KX<=q z-PhOm`?o%a7TzB>UWR~TGgB41`m1RY<(JsVfN|Vm_UU^b*Hf*XW3GNzne499yxR9F z^n2ngKl}Z zwx;I*HP!?W)ZDjkLs>l$=3a8OSV`B>ZE(gyTfPcz5d?_=d-AE!zIz$oQm4Fq8Sfqb^W>Kjsr4P=D{W6QkJ=%5 zi%yvKU3daTj#0O8Wl@lt{2p1=xm$6L5>NUoW>?xE8E^|gY+pJH@=M(7fAzS(jFY!9 zYg(ICp`&hUyIm3X6zQ!*P;pZNDxsiWElsH{{`+-IOF+%#Df)pF{&G#U?}2;M+aP!Q z&jC$R1<1bHv(7em*zZ`V(CpwD5+kZT`4IRUko*HZPLz7M+v)r@?pW}%hJ~nGW@9}> zFMiY53dv9g8O??|Wlw$c`Li!4(AyTOWdHrw9w4I{_4?h-aB~_%j#~_hA^C`R%~dy9 z{DwXTs!~Bd@PO%l8&-V%FGUhr{vUmh$=I@^FLc4)yhWt--hP+J*{4DO=sJ*7Jzy_P z(lYL=zMAEhgC3s!$L~Se53~dG6XbJw|Gm={K>gETvE|fvtqhWHOx8Ag1E7^X2ogMM zGIu5aaRcty81R<<$+#QTtQ`BhkJuJSmn}$ie$R&_{Nq7(g2bk%(1(5hczEv6Wo^Co zDMYW90yWrM9Sii*asYVvSV4^c(NB=Pg~sl1+W!CLPNoWs;v9X$@md9%UwD4#U*8VV z^Z2g;(9}dN`86`NZ%n*6vilkTElFUR|Mt{(fOM{r$(bKh))zmryVVGo#=ngyH<2cb zrn5Y%cth@LNy~2e$1vJ}TA^o8)8AE(v90%f?BxG%_pTiZT=}E1Li={lS8js+h{S)t zzdO4pP>8s6KX8|c9_K%AU@#0yIMK7t93Fk0BMKYHg+w06-3hK`|24TaYQeJAXcu|% zo*z8!-yJQ1P6g6s|6da=6;4GT?+hg_YMvFA0%t4ohupI@x8C{>Q2}mblFfNS9cx~+|XeB{m33UJ2D)2^YlKxHar_i+4_tOYGz-6 zGSJ_a51qDOub5B(s=0Qc91tMx5c=Q?{YvMrWZ52aAAdiHv?S12@$L08O>UJs=H z{Z-W-A-(oV^mqPsU&t4g$|B7-K#6t6vt_6p(ZS83G9pz$Kctf)pA+*3 zulZT*>Z+=@nd2*!KZBOiF2|;eTt<(&&8&`dI3OJhc@+gRZOIa1~7aX#lJx^Kv5xR^1`ns<3Z8zZz0Kzx(6fFA$Q;J1JW8G7ZV`P z?8vws5D_>v;)F-GXQo%ZS)%}12xQY-K%HTiPdggYaqXSXfD3uBO&@og5!dpM_La-F z1{Z_#ZSdSGVb-Pl5%yd|*<1=hKL$m=5&H08y2@t8_@y{IOmZkRa9=-n?7Uo_^IZzQ{h_PbS{{q73V~TF}ik|I;T9Jw0}e=e7{v z(R-!Wj^DfX=fIyI4r*@wZQF6{?e`Au*>?_HPCje3TDm&NB4TkMov9vP*{zNo6~-Xb z2xZJW+9WdVjbS8o>hSYe9tZ<&N8AOJxnQJDpi6dqD@SZ}YU|9iY$6I{_#uqz|M4l_ z)n~}DcN>I|QQ`r1R%tMS`-#@Pr6uD}$L7e|no@CuMZ?RG*L-E>2|SR~>7Y*ea|LY6 z9S`-aV3*}}TOElocS}z#$ZJf`nKNQwkqrTx{#OfA3H&O(3mTquN61}ENjV9^cd%69 zzX_c7^@Kr&Z9DRpB9O(5qFadmh1Kqlo$i#aAHrTH#Lz&UW0(AWzSq^2GIu@YTtr91 zBE04B3o{A*440*rSQckg#KsG_X&!vzV}~^WMX6>ABwNn zvA=g%D&V_WGtn6LTkyvLsQ>)Ii_yV)y#F#iZF+4CJM0e5dno&(`^l>2@N$^FER}6Y zeJKe1#w_ruk3EXE4)psC{rvv%Sl{ZdIpoY>hc{8nRvY&dPsHSx9D`h`b#V;Mvus4aY2NU6p<5jYWi0CneUeiKxb#CjPhEa!57nG~1fB zm1sK;N=}bk&p#~!c$u%51d@85JuYZ`Xh`j3D_HosV28V6ZzuGj5?G)#qbXFPzjeYh zT&Ll6?0{p$yz@~IX_-_;D zhnRDfV~{cssQo>zuYi6E^5G89Z2mQu5Xd-CZb42D|826O5IuEn!21P;q|Iapc zP#)e_vPRnoSgAf$9|}jP6Gws^J=GxJdawD}gs?{Xm4PVs6>zPOLs4x@g=11|T`?W` z>2#7k?2?dH*9Neg4+U?}u%_&YR^yB;5yujNxFPIqpfo?TS%3~itlaW*?)3~flFjRn ze@TJ3W$fJR|L#{@2ftGsgASHdGID@yw=vzm4dH@00tp-ixem1)K8Bp>%ee#R{!0hxw z!&cbcq~kJ?5Fg~2EihIfWxTsx^SZAHZl7zCO3AL=HXg`V@y#Ay>~cz5VG2|QtdH4| z=k5$HId~47US~^mPNmF8`g_nQK0l%{8lk%CBkD^Jb^D{3{%g~^FoL*_$}+mO3b)#s zir1!uR?H7)7}?F-PO_fMtU}BdWz#HXP%^XyaF@jH-Cn_!DqlO+x;qPfM~7JYo&(Z4 zI$_00^g&&GxymW*@^dNngztd}ME_taek~b~$w_xNAs&ZYzM=-K-Pjx{a4r+$^C$9% z6>P~NJk)^O#VJjf0Ng5F7o8K=FzX?XUSN+*5Iq{$gIEs7eP}uXllBsw@v*9NP2Y2^ zl7%+gAD5VGFrv8CR98N0PLc0fW*>uUS5E~>Ir<&eTt6Ae`-RB~;Fxd-( zrF*O>2$KfXeSS=)xH*RS8&Gu>u3Zzbk)*NhtX(4cJS0M~3~%asCe*AGIT}Xgq-Hsy zNMhhvD>)c(c)Unra*YtdDc2l*sgPFOTO2yZ@^Vm`38l(S3J$Lf+4XsAGYY@AMlMV* z3)j`IW_jgUHk22qhU(6tr)zd%l3tP%$-!0rqmsk`YFp=4%lB_aYI zBUMFO^_d;)-)B5$Yg5#fqve&9)>GHR;&}~MbUlLXy!)^O^%LPArS6)c_fdOV#p>3( zhXqfq50ASkqgxf*$!-$jE9FStPrDWBve|H`lF#AwwIc5zf)VCe=#oV7!Z%-(-+X+< zks!COj6WQ5SV-SC%~X2&a+`ZKTD^@PUW#<=92=f1j9SK?aI=#wA)n!EanXOI0iQc!o)lAGeaGcMDj?9lgy*H z-wQ7n?kiTRPnpUr@|p}=F}XD`4W(}@=qc86wdXyZB0RoYreOM;Mo`3}p=!iFV)*O@gBQoRf86mpNfbn zdOtvk|4qP%3VWfzdwVSKNW%PvKP}Te#etap;?}N64_=7V&>p4NQb=9_l{;Yt>=M-r zwLQ5`_j;_0uiJ295e0Mb<>2^(k$gK2<}c)^UYV|v37Ru04JAv#l4Mf~?OCSv1hhkC zrY0sAo$)YMoF0Z;jGpsuCkmTw3y zL_v0X{CzUL5Vm%;@MjLe&~|TcU5_v1t9+s^3UI)u0^dZ?n6rJ}$~R$X+Qv%o{nn9z z3Ws}JV*DwEt5~-w<(!4uyzk6PS3M3(V!ky!w^Evff}E1A|Ur zmpmzIwup3e6NKjLmyofwnY6EMRu4=mHJ@tSXW4UgR`0G3U8majns^ju4Dw4zOv#^T z#igT82|)7=?#Xn`i}4Nz(MxifjL)EjXpfXcx&{-;r;?Z5=5`XBvNcaQF%ek#6mHR6 zyLzNn@0Ojss}p^OT`IF*V(6#6ax4_@Ta6Twh9CIR7rIaDPH%=je)_sB5<;Sz5RRhq1Ndc%jiSApCXM^Sm?xP zpLyi4e&n*wfMX2db2_yZrgqWoou-Q=uSq8pd-brueo%*h-OV?JlP+G88~E%4IFhdI z+#2ekek7lyLX}KoTghF5@h+&^UD~KE(&lNCVI-e>vAYK1T^zce^&{&>=0Yo?31cRX zfhxP2BgI;}HMeNN4ZAV|93)viSpn=4gJG@MbsJl-sL5xdKK{L68~6lzNoP|{5!CpJ zjCR~Y*hWWI8H$pMwaA7neHP(?;%6WVvocwq8ED~~8E-8?WO%9&;% zrQjRWOSTWZuKn$d7l%8iqLHG~astp5i{I+Mn8APa4eQa*nA-wxQzIj@YK03K$d2R_ zcC0OCb5^MiXt>19$ok{1<{Fq6VVtp6kwS7ZS!0mkG_5HmzFR$BpJ#I$J`c9xLn^Kr z)SFCA9SNYFkZsrJQBLFBy0;i>5_Z@I{pqApG&m|4m!X8uHeNgr(ZOoXB14~6$Jb)B z_Bn@VyQ1r}+7t2e&et~gwm-q%rg*2zi8#tbQax8h{D|?c&RNyOZJUo>l_=>^GJ{zw z*wxi(rtxgA*^wtL$0xA%`|0u7rg)D+rSMNiZd2DY%+#ma$JWR6cR?q^cM3X5V~{_N zRH26i)6y5Fo@6#Lb{3!5t29tUe`(w3Wu9c9;Rq=MqyK2`ZnNGUaKr+sd2*N}L-c-7|rEb@c3^|7wg*|_) z5O(!}LHfcI3T<^1+8J*g@}uP?x!_>}&=Zum^OSACvIkpayYACzrbw@N+>#RplV24Umt<(sP?T$%Wr%o8F5n zFjno8s%O5&f3JD3j1aRJEc{kVEqD(P(G6SMP4ZnU7(KzhaFbSN#K`gZ^S*%l+O*>C zdQAh0y$`DeW2^*!X_TmV>R+*IN+KqHpoGoXN8ltSzs}R`Nxns{oaHG~~wMP+1;^Ykuve zO^OOkl}C6cj8BrYz)8DsD&i zs4o@nXF{bVRt8y*R^f(M?-2SuLGbzcugDA{*UiY;c@R?!n^?pw#S1L}!1^yB=-SAI4Lp2H9dn$D`OW(h zK*r&O7Zn#-arfTr#^N>4Fz&xU1RMeAsWyy7s&Kj|hhuykTH*ftE)5h7>f}(lnYk@ zw(I|Oh}PXf!1|Z`^+KjHKvLTR@`!ug27tXB@K4;ozj7%#ZoMVAUUNBpz%z3HK5h5^ zrW=&gf6gmtMq7`NuOpYSL!~FUaji*~8{r1G+m64wdN)YjDT85h?`}Zw{+J@SZQOyn z`|GJ)&2}9+W&5sjcqIaCL>*jQixfWv0=vwO_@%LFdXyXS*J)p1U_b?YSFHU9Co0Yi z>sIe78e{woGz8&}rJ;xv0LWQwyu12!YiokmnOSN4zX47S0_GlkD9Qa$bMT9Vj+0Z8 zihyV~K!=bIG{7m?zErsksFX{2e5kPFN44qy!ggU+`aR?AE@bwcE}~=QV@<^(DmH8$ z=0cZxsM);!vw7&WQY}Dr8WHm)-U#uLpLbM&pl!v0O7GC9Rq-l!?!>ucJ-i)YB4Ezg zfT-xtj(HxSc6Z)y_q`M13MO%r@U9sQ&i@j%c4M7li`1>cgjBnK)~RAKLCgM|@GOY1 z2~xvWze@Br#f%OzZzz+5jJ=BlK;Jolp!Vo#x~w7kVWqCp(`Gj1zd)LPq#Jl@$laFB z+X`&n_GC!ny}uyWhK1i^t;wYYKJ8AmZ>8kYPPWUYZXEHl zt;uEVChO*vad%jkuD-jgTzs}9#x7<|)YcZ>m{$)}U!^7nb=3Yyn$g;X5EH)r>cTLA zPXQs_Z?l>;qZ7#8aCH9L4FJjfn?gZ6o9bvEa$aa#mzqmjIEvbx=P)_=`YVXOaZA=z zE;3siC$EJoOT;kRE~*9zN@&iHKMpk0a;yi(8N8B;nRXMBz7{qN@6#&MTAOs~TcP=7 z8KTpdy4820C+*+;=$GlVmCC7q=j_$_QhRlGAS#gbzHkdBWP{4PQ&Yh!#HB;6c1e8l zlx^jE_irAeoO)*3LydHCPNrI%t&|uKK)bgQP&v!#QQ0^X{YcP4(82YI+`09YmPVT( zfaRN0e1RbX`<3>5l-;o9Y3Kb$Sukc?43l_Fo{M2Rj&KpMSERK7O8L-50UFgYwAKhe zKF2?CB`YeWslEL?0ees8p|1D(fgiQCx>G726l&HdIso1*L>2jleEagwJ!SW9aat~? zGbdU@*d7J%^Xa3<;SMKs5d8EbcVep1Npov$=#m8Lr3GeHF*-_jwY9(Sdl5JuilPSjZnQ0`z$IUbUZ0|0WXQb*BJ1fMIgkTCP1ptyVUjYvL$ z(J*g>YI{tbr0iTlyK2sn2iP=^kf{?Gn&_RMQmpc?A~xuP9`Z&hBGT4RpJ(+O<$T}! z3zSB)v*12nt{dzqJ1n6^mqRowT0HdV%bU;a0)2GG#VZUI8_YINzw>r>ecG@00J-=!BPDLsp8PD?7QkI!}3>x%;02m)*! zN##dh2=brSkyJQ$N_i@J59ynY@M6F~2EZw)sA)|)%#BD!Av|%F%P$*XiNP_6NOyt< zXolOx* zn27I)*i*xYS9kRxExaUs4&=8L`05Ml1z?T>m+P~AhUe;2lj(JMqv#m-?z+Bbj_+>6 zmj{DMLS|Gqw=Sn;11a%~=I5;yf~+vxq{F3kj2F_L1a(Jo|>5S+?GzHbbD87X_$3ha)@D>fK#|S+?*E z&N&_bW2iwf4-bZtvbD1~^F>49JM3DlR}$H657E1Ci0Kb)$i!ThHi0EdqVHTEzw3@8qIpQlJ4bCs8`*t1UNe`b7Tn4X~ne_e35)3?b zIsI7�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 literal 0 HcmV?d00001 diff --git a/documentation/snapshot/docs/contributing/code-of-conduct.md b/documentation/snapshot/docs/contributing/code-of-conduct.md new file mode 100644 index 0000000000..84960873d9 --- /dev/null +++ b/documentation/snapshot/docs/contributing/code-of-conduct.md @@ -0,0 +1,27 @@ +At Pinterest, we work hard to ensure that our work environment is welcoming and inclusive to as many people as possible. We are committed to creating this environment for everyone involved in our open source projects as well. We welcome all participants regardless of ability, age, ethnicity, identified gender, religion (or lack there of), sexual orientation and socioeconomic status. + +This code of conduct details our expectations for upholding these values. + +## Good behavior + +We expect members of our community to exhibit good behavior including (but of course not limited to): + +- Using intentional and empathetic language. +- Focusing on resolving instead of escalating conflict. +- Providing constructive feedback. + +## Unacceptable behavior + +Some examples of unacceptable behavior (again, this is not an exhaustive list): + +- Harassment, publicly or in private. +- Trolling. +- Sexual advances (this isn’t the place for it). +- Publishing other’s personal information. +- Any behavior which would be deemed unacceptable in a professional environment. + +## Recourse + +If you are witness to or the target of unacceptable behavior, it should be reported to Pinterest at opensource-policy@pinterest.com. All reporters will be kept confidential and an appropriate response for each incident will be evaluated. + +If the maintainers do not uphold and enforce this code of conduct in good faith, community leadership will hold them accountable. diff --git a/documentation/snapshot/docs/contributing/guidelines.md b/documentation/snapshot/docs/contributing/guidelines.md new file mode 100644 index 0000000000..f7fcd7acf7 --- /dev/null +++ b/documentation/snapshot/docs/contributing/guidelines.md @@ -0,0 +1,59 @@ +First off, thanks for taking the time to contribute! This guide will answer some common questions about how this project works. + +While this is a Pinterest open source project, we welcome contributions from everyone. Regular outside contributors can become project maintainers. + +## Help + +If you're having trouble using this project, please start by reading all documentation and searching for solutions in the existing open and closed issues. + +## Security + +If you've found a security issue in one of our open source projects, please report it at [Bugcrowd](https://bugcrowd.com/pinterest); you may even make some money! + +## Code of Conduct + +Please be sure to read and understand our [code of conduct](../code-of-conduct/). We work hard to ensure that our projects are welcoming and inclusive to as many people as possible. + +## Reporting Issues + +If you have a bug report, please provide as much information as possible so that we can help you out: + +- Version of the project you're using. +- Code (or even better a sample project) which reproduce the issue. +- Steps which reproduce the issue. +- Stack traces for crashes. +- Any logs produced. + +## Making Changes + +!!! tip + `ktlint` only provides rules that enforce the [Kotlin coding conventions](https://kotlinlang.org/docs/coding-conventions.html) or [Android Kotlin style guide](https://developer.android.com/kotlin/style-guide). If your change is more opinionated than please [file an issue](https://github.com/pinterest/ktlint/issues/new) first so that it can be discussed amongst the community. Rules which are too opinionated might be better published as a custom rule set. + +1. Fork this repository to your own account +2. Make your changes and verify that tests pass +3. Commit your work and push to a new branch on your fork +4. Submit a pull request +5. Participate in the code review process by responding to feedback + +Once there is agreement that the code is in good shape, one of the project's maintainers will merge your contribution. + +To increase the chances that your pull request will be accepted: + +- Follow the coding style +- Write tests for your changes +- Write a good commit message +- Provide context in the pull request description. + +New rules have to implement the `Rule.Experimental` interface so that the rule will only be run for user who have opted in to use experimental rules. Once the rule is stable, the marker interface `Rule.Experimental` can be removed. + +## Updating dependencies + +This project has enabled [Gradle dependencies verification](https://docs.gradle.org/6.2/userguide/dependency_verification.html). On adding/updating any dependency, ensure that you've added dependency provided checksum/signature to `gradle/verification-metadata.xml` file. + +## Using kotlin development versions + +Add following flag - `-PkotlinDev` to enable kotlin development version. + +## License + +By contributing to this project, you agree that your contributions will be licensed under its [license](/#legal). diff --git a/documentation/snapshot/docs/contributing/index.md b/documentation/snapshot/docs/contributing/index.md new file mode 100644 index 0000000000..e5c323966a --- /dev/null +++ b/documentation/snapshot/docs/contributing/index.md @@ -0,0 +1 @@ +## Contributing guidelines diff --git a/documentation/snapshot/docs/contributing/overview.md b/documentation/snapshot/docs/contributing/overview.md new file mode 100644 index 0000000000..cb4aca74de --- /dev/null +++ b/documentation/snapshot/docs/contributing/overview.md @@ -0,0 +1,19 @@ +!!! important + Make sure to read the [Contributing guideline](guidelines.md) and the [code of conduct](code-of-conduct.md) first. + +## Development + +Development starts with cloning and building the project on your local machine: + +```sh +git clone https://github.com/pinterest/ktlint && cd ktlint +./gradlew tasks # shows how to build, test, run, etc. project +``` + +!!! tip + To open and run `ktlint` in Intellij IDEA: + + * File -> Open.... + * You'll also need to set the "Project language level" to 8 in "Project Settings" (File -> Project Structure... -> Project). + * To run `ktlint` - right-click on `ktlint/src/main/kotlin/com/pinterest/ktlint/Main.kt` -> Run. + diff --git a/documentation/snapshot/docs/faq.md b/documentation/snapshot/docs/faq.md new file mode 100644 index 0000000000..9f47b60e13 --- /dev/null +++ b/documentation/snapshot/docs/faq.md @@ -0,0 +1,186 @@ +## Why should I use ktlint? + +the short answer is **Simplicity**. + +Spending time on configuration (and maintenance down the road) of hundred-line long style config file(s) is counter-productive. Instead of wasting your energy on something that has no business value - focus on what really matters (not debating whether to use tabs or spaces). + +By using ktlint you put the importance of code clarity and community conventions over personal preferences. This makes things easier for people reading your code as well as frees you from having to document and explain what style potential contributor(s) have to follow. + +ktlint is a single binary with both linter & formatter included. All you need is to drop it in (no need to get [overwhelmed](https://en.wikipedia.org/wiki/Decision_fatigue) while choosing among [dozens of code style options](https://checkstyle.sourceforge.net/checks.html)). + +## How do I enable or disable a rule? + +An individual rule can be enabled or disabled with a rule property. The name of the rule property consists of the `ktlint_` prefix followed by the rule set id followed by a `_` and the rule id. Examples: +```editorconfig +ktlint_standard_final-newline = disabled # Disables the `final-newline` rule in the `standard` rule set provided by KtLint +ktlint_standard_some-experimental-rule = enabled # Enables the (experimental) `some-experimental-rule` in the `standard` rule set provided by KtLint +ktlint_custom-rule-set_custom-rule = disabled # Disables the `custom-rule` rule in the `custom-rule-set` rule set (not provided by KtLint) +``` + +!!! note + The *rule* properties are applied after applying the *rule set* properties and take precedence. So if a rule set is disabled but a specific rule of that rule set is enabled, then the rule will be executed. + +## How do I enable or disable a rule set? + +All rules in a rule set can be enabled or disabled with a rule set property. The name of the rule set property consists of the `ktlint_` prefix followed by the rule set id. Examples: +```editorconfig +ktlint_standard = disabled # Disable all rules from the `standard` rule set provided by KtLint +ktlint_experimental = enabled # Enable rules marked as experimental for all rule sets that are enabled +ktlint_custom-rule-set = enabled # Enable all rules in the `custom-rule-set` rule set (not provided by KtLint) +``` + +!!! note + All rules from the `standard` and custom rule sets are *enabled* by default and can optionally be disabled in the `.editorconfig`. All rules from the `experimental` rule set are *disabled* by default and can optionally be enabled in the `.editorconfig`. + +## Can I have my own rules on top of ktlint? + +Absolutely, "no configuration" doesn't mean "no extensibility". You can add your own ruleset(s) to discover potential bugs, check for anti-patterns, etc. + +See [adding a custom rule set](../api/custom-rule-set/) for more information. + +## How do I suppress errors for a line/block/file? + +!!! tip + Suppressing a `ktlint` violation is meant primarily as an escape latch for the rare cases when **ktlint** is not able to produce the correct result. Please report any such instances using [GitHub Issues](https://github.com/pinterest/ktlint/issues)). + +To disable a specific rule you'll need the rule identifier which is displayed at the end of the lint error. + +An error can be suppressed using: + +* EOL comments +* Block comments +* @Suppress annotations + +=== "[:material-heart:](#) Suppress annotation" + + ```kotlin + // Suppressing all rules for the entire file + @file:Suppress("ktlint") + + // Suppress a single rule for the annotated construct + @Suppress("ktlint:standard:no-wildcard-imports") + import foo.* + + // Suppress multiple rules for the annotated construct + @Suppress("ktlint:standard:no-wildcard-imports", "ktlint:standard:other-rule-id") + import foo.* + + // Suppress all rules for the annotated construct + @Suppress("ktlint") + import foo.* + ``` +=== "[:material-heart:](#) EOL comments" + + ```kotlin + // Suppress a single rule for the commented line + import foo.* // ktlint-disable standard_no-wildcard-imports + + // Suppress multiple rules for the commented line + import foo.* // ktlint-disable standard_no-wildcard-imports standard_other-rule-id + + // Suppress all rules for the commented line + import foo.* // ktlint-disable + ``` + +=== "[:material-heart-off-outline:](#) Block comments" + + ```kotlin + // Suppress a single rule for all code between the start and end tag + /* ktlint-disable standard_no-wildcard-imports */ + import foo.* + /* ktlint-disable standard_no-wildcard-imports */ + + // Suppress multiple rules for all code between the start and end tag + /* ktlint-disable standard_no-wildcard-imports standard_no-wildcard-imports */ + import foo.* + /* ktlint-enable standard_no-wildcard-imports standard_no-wildcard-imports */ + + // Suppress all rules for all code between the start and end tag + /* ktlint-disable */ + import foo.* + /* ktlint-enable */ + ``` + +!!! important + When using the block comments, the `ktlint-enable` directive needs to specify the exact same rule-id's and in the same order as the `ktlint-disable` directive. + +!!! warning + From a consistency perspective seen, it might be best to **not** mix the (EOL/Block) comment style with the annotation style in the same project. + +## How do I globally disable a rule without `.editorconfig`? + +When using Ktlint CLI, you may pass a list of disabled rules via the `--disabled_rules` command line flag. The value is a comma separated list of rule id's that have to be disabled. The rule id must be fully qualified (e.g. must be prefixed with the rule set id). + + +## Why is `.editorconfig` property `disabled_rules` deprecated and how do I resolve this? + +The `.editorconfig` properties `disabled_rules` and `ktlint_disabled_rules` are deprecated as of KtLint version `0.48` and are removed in version `0.49`. Those properties contain a comma separated list of rules which are disabled. Using a comma separated list of values has some disadvantages. + +A big disadvantage is that it is not possible to override the property partially in an `.editorconfig` file in a subpackage. Another disadvantage is that it is not possible to express explicitly that a rule is enabled. Lastly, (qualified) rule ids can be 20 characters or longer, which makes a list with multiple entries hard to read. + +Starting with KtLint `0.48` entire rule sets and individual rules can be disabled / enabled with a separate property per rule (set). Examples: +```editorconfig +ktlint_standard = disabled # Disable all rules from the `standard` rule set provided by KtLint +ktlint_standard_final-newline = enabled # Enables the `final-newline` rule in the `standard` rule set provided by KtLint +ktlint_experimental = enabled # Enable rules marked as experimental for all rule sets that are enabled +ktlint_standard_some-experimental-rule = disabled # Disables the (experimental) `some-experimental-rule` in the `standard` rule set provided by KtLint +ktlint_custom-rule-set = enabled # Enable all rules in the `custom-rule-set` rule set (not provided by KtLint) +ktlint_custom-rule-set_custom-rule = disabled # Disables the `custom-rule` rule in the `custom-rule-set` rule set (not provided by KtLint) +``` + +!!! note + All rules from the `standard` and custom rule sets are *enabled* by default and can optionally be disabled in the `.editorconfig`. All rules from the `experimental` rule set are *disabled* by default and can optionally be enabled in the `.editorconfig`. + +!!! note + The *rule* properties are applied after applying the *rule set* properties and take precedence. So if a rule set is disabled but a specific rule of that rule set is enabled, then the rule will be executed. + +## Why is wildcard import `java.util.*` not reported by the `no-wildcard-imports` rule? + +The `no-wildcard-imports` rule forbids wildcard imports, except for imports defined in `.editorconfig` property `ij_kotlin_packages_to_use_import_on_demand`. If this property is not explicitly set, it allows wildcards imports like `java.util.*` by default to keep in sync with IntelliJ IDEA behavior. + +## Can I use KtLint to directly format the code I'm generating with KotlinPoet? + +Yes, it is possible to use KtLint to directly format the code generated with KotlinPoet. +To do so, you must include the dependencies `com.pinterest.ktlint:ktlint-core` and `com.pinterest.ktlint:ktlint-ruleset-standard` in your Gradle/Maven project. + +!!! warning + Do not include the dependency `com.pinterest:ktlint` as that would import the entire ktlint project including unwanted dependencies. Besides a much bigger artifact, it might also result in problems regarding logging. + +To format the output of KotlinPoet with KtLint, you can use the following snippet: + +```kotlin +val ruleProviders = buildSet { + ServiceLoader + .load(RuleSetProviderV2::class.java) + .flatMapTo(this) { it.getRuleProviders() } +} +val ktLintRuleEngine = KtLintRuleEngine( + ruleProviders = ruleProviders, + editorConfigDefaults = EditorConfigDefaults.load(EDITORCONFIG_PATH), +) +ktLintRuleEngine.format(outputDir.toPath()) +``` +Here, outputDir refers to the directory of the generated files by KotlinPoet, ktLintRuleEngine is an instance of KtLint rule engine. + +It is also possible to format file-by-file the output of KotlinPoet if you write your `FileSpec` to a `StringBuilder()`, instead of a `File`, +and send the generated code as `String` to KtLint inside a `CodeSnippet`: +```kotlin +kotlinFile.writeText( + ktLintRuleEngine.format( + Code.CodeSnippet( + stringBuilder.toString() + ) + ) +) +``` + +# Are formatter tags respected? + +As of version `0.49.x` the formatter tags of IntelliJ IDEA are respected. By default, those formatter tags are disabled. The formatter tags can be enabled with `.editorconfig` properties below: +```editorconfig +ij_formatter_tags_enabled = true # Defaults to 'false' +ij_formatter_off_tag = some-custom-off-tag # Defaults to '@formatter:off' +ij_formatter_on_tag = some-custom-on-tag # Defaults to '@formatter:on' +``` + +When enabled, the ktlint rule checking is disabled for all code surrounded by the formatter tags. diff --git a/documentation/snapshot/docs/index.md b/documentation/snapshot/docs/index.md new file mode 100644 index 0000000000..85e6cecc62 --- /dev/null +++ b/documentation/snapshot/docs/index.md @@ -0,0 +1,41 @@ +# Welcome to Ktlint + +

+ + + +

+

+Join the chat at https://kotlinlang.slack.com +Build status +Maven Central +ktlint +

+

+Kotlin linter in spirit of feross/standard (JavaScript) and gofmt (Go). +

+ +## Features + +- **No configuration required** + `ktlint` aims to capture the [Kotlin coding conventions](https://kotlinlang.org/docs/reference/coding-conventions.html) and [Android Kotlin Style Guide](https://android.github.io/kotlin-guides/style.html). In some aspects `ktlint` is a bit more strict[*](https://github.com/pinterest/ktlint/issues/284#issuecomment-425177186). +- **Rule sets** + `ktlint` offers a `standard` rule set. Next to this, it is easy to provide [custom rule sets](api/custom-rule-set/). +- **.editorconfig** + Some rules do allow further configuration, but in all cases a reasonable default is set when not provided. `ktlint` primarily uses the [.editorconfig file](rules/configuration-ktlint/) to read default `.editorconfig`, IntelliJ IDEA specific and Ktlint specific properties. +- **Disable rules** + If need be, rules can be disabled easily[*](faq/#how-do-i-globally-disable-a-rule). +- **Built-in formatter** + Most lint violations don't need to be fixed manually. `ktlint` has a built-in formatter which fixes violations when possible. Some violations can not be fixed in a deterministic way, and need manual action. +- **Customizable output** + Several reporters are available out-of-the-box: `plain` (+ `plain?group_by_file`), `plain-summary`, `json`, `html` and `checkstyle`. + It's also easy to [create a custom reporter](api/custom-reporter/). +- **Executable jar** + `ktlint` is released as a single executable jar with all dependencies included. + +## Legal + +This project is not affiliated with nor endorsed by JetBrains. +All code, unless specified otherwise, is licensed under the [MIT](https://opensource.org/licenses/MIT) license. +Copyright (c) 2019 Pinterest, Inc. +Copyright (c) 2016-2019 Stanley Shyiko. diff --git a/documentation/snapshot/docs/install/cli.md b/documentation/snapshot/docs/install/cli.md new file mode 100644 index 0000000000..59fd63ecf2 --- /dev/null +++ b/documentation/snapshot/docs/install/cli.md @@ -0,0 +1,222 @@ +!!! note Command Line usage + If you don't plan to use `ktlint`'s command line interface then you can skip this section. + +## Download and verification + +### Download manually from github + +All releases of `ktlint` can be downloaded from the [releases](https://github.com/pinterest/ktlint/releases) page. + +### Download using curl + +A particular version of `ktlint` can be downloaded with next command which also changes the file to an executable in directory `/usr/local/bin`: + +```sh title="Download" +curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.49.0/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/ +``` + +!!! tip "Curl not installed or behind proxy" + If you don't have curl installed - replace `curl -sL` with `wget -qO-`. + If you are behind a proxy see - [curl](https://curl.haxx.se/docs/manpage.html#ENVIRONMENT) / [wget](https://www.gnu.org/software/wget/manual/wget.html#Proxies) manpage. Usually simple: + ```shell + http_proxy=http://proxy-server:port https_proxy=http://proxy-server:port curl -sL ... + ``` + +### Verification of download + +`ktlint.asc` contains PGP signature which you can verify with: + +```sh title="Verify releases 0.32.0 and above" +curl -sS https://keybase.io/ktlint/pgp_keys.asc | gpg --import && gpg --verify ktlint.asc +``` + +```sh title="Verify releases up through 0.31.0" +curl -sS https://keybase.io/shyiko/pgp_keys.asc | gpg --import && gpg --verify ktlint.asc +``` + +### Package managers + +`ktlint` can be installed via several OS specific package managers. + +Install with [brew on macOS](https://brew.sh/) or [Homebrew on Linux](https://docs.brew.sh/Homebrew-on-Linux) +```sh +brew install ktlint +``` + +Install with [MacPorts](https://www.macports.org/) +```sh +port install ktlint +``` + +Install with [SDKMAN! on macOS and Linux](https://sdkman.io/) +```sh +sdk install ktlint +``` + +On Arch Linux install package [ktlint AUR](https://aur.archlinux.org/packages/ktlint/). + +## Command line usage + +### Rule set(s) + +When no arguments are specified, the style of all Kotlin files (ending with '.kt' or '.kts') inside the current dir (recursively) are validated with the (non-experimental) rules from the [standard ruleset](../../rules/standard/). Hidden folders will be skipped. + +```shell title="Default validation with standard ruleset" +ktlint +``` + +To validate with the [standard ruleset](../../rules/standard/) including the experimental rules run command below: + +```shell title="Validation with standard ruleset including the experimental rules" +ktlint --experimental +``` + +!!! note + Instead of using this command line flag, it is advised to set `.editorconfig` property `ktlint_experimental = enabled` if you want the project always to be checked with the experimental rules. + +To validate with a [custom ruleset](../../api/custom-rule-set/) run command below: + +```shell title="Validation with standard and a custom ruleset" +ktlint --ruleset=/path/to/custom-ruleset.jar +# or +ktlint -R /path/to/custom-ruleset.jar +``` + +!!! note + If the custom rule set contains rules that are marked as experimental, those rule will only be run when `.editorconfig` property `ktlint_experimental = enabled` is set (or command line parameter `--experimental` is specified). + +### Format (autocorrect) + +Most style violations can be corrected automatically. Errors that can not be corrected, are printed to `stderr`. + +```shell title="Autocorrect style violations" +ktlint --format +# or +ktlint -F +``` + +### Globs + +Globs can be used to specify more exactly what files and directories are to be validated. `ktlint` uses the [`.gitignore` pattern style syntax for globs](https://git-scm.com/docs/gitignore). Globs are processed from left to right. Prepend a glob with `!` to negate it. Hidden folders will be skipped. + +```shell title="Check only certain locations starting from the current directory" +# Check all '.kt' files in 'src/' directory, but ignore files ending with 'Test.kt': +ktlint 'src/**/*.kt' '!src/**/*Test.kt' + +# Check all '.kt' files in 'src/' directory, but ignore 'generated' directory and its subdirectories: +ktlint 'src/**/*.kt' '!src/**/generated/**' +``` + +### Violation reporting + +`ktlint` supports different type of reporters for lint violations. When not specified the `plain` reporter is used. Optionally the `plain` reporter can group the violations per file. + +```shell title="Style violation grouped by file" +$ ktlint --reporter=plain?group_by_file +``` + +When using `ktlint` on an existing project, the number of violations can be huge. To get more insights in which rules are causing the most violations, the `plain-summary` reporter can be used. +```shell title="Style violations counted per rule" +$ ktlint --reporter=plain-summary +``` + +Other built-in reporters are: `json`, `sarif`, `checkstyle`, and `html` + +Style violations can be written to an output file which is convenient when multiple reporters are specified. In example below, the plain reporter is used to write to the console while the checkstyle reports is written to a file: + +```shell title="Multiple reporters" +ktlint --reporter=plain --reporter=checkstyle,output=ktlint-report-in-checkstyle-format.xml +``` + +If resolving all existing errors in a project is unwanted, it is possible to create a baseline and in following invocations compare violations against this baseline. Violations that are registered in the baseline, will be ignored silently. Remove the baseline file in case you want to reset it. + +```shell title="Check against a baseline file" +ktlint --baseline=ktlint-baseline.xml # Baseline is created when not existing +``` + +### Logging + +Logging information is written to `stdout`. The amount of logging can be influenced by setting the minimal log level using option `--log-level` or `-l` to one of values `trace`, `debug`, `info`, `warn`, `error`, or `none` to suppress all logging. + +By default, the `info` log level is used meaning that all log lines at level `info`, `warn` and `error` are shown while suppressing log lines at level `debug` or `trace`. + +### Rule configuration (`.editorconfig`) + +Some rules can be tweaked via the [`editorconfig file`](../../rules/configuration-ktlint/). + +A scaffold of the `.editorconfig file` can be generated with command below. Note: that the generated file only contains configuration settings which are actively used by the [rules which are loaded](#rule-sets): + +```shell title="Generate .editorconfig" +ktlint generateEditorConfig +# or +ktlint --experimental generateEditorConfig +# or +ktlint --experimental --ruleset=/path/to/custom-ruleset.jar generateEditorConfig +``` + +Normally this file is located in the root of your project directory. In case the file is located in a sub folder of the project, the settings of that file only applies to that subdirectory and its folders (recursively). Ktlint automatically detects and reads all `.editorconfig` files in your project. + +Use command below, to specify a default `editorconfig`. In case a property is not defined in any `.editorconfig` file on the path to the file, the value from the default file is used. The path may point to any valid file or directory. The path can be relative or absolute. Depending on your OS, the "~" at the beginning of a path is replaced by the user home directory. + +```shell title="Override '.editorconfig'" +ktlint --editorconfig=/path/to/.editorconfig +``` + +!!! warning "Overrides '.editorconfig' in project directory" in KtLint 0.46 and older + When specifying this option using ktlint 0.46 or older, all `.editorconfig` files in the project directory are being ignored. Starting from KtLint 0.47 the properties in this file are used as fallback. + +### Stdin && stdout + +With command below, the input is read from `stdin` and the violations are printed to `stderr`. Logging is written to `stdout`. + +```shell title="Lint from stdin" +ktlint --stdin +``` + +When combined with the `--format` option, the formatted code is written to `stdout` and the violations are printed to `stderr`: + +```shell title="Format from stdin and write to stdout" +ktlint --stdin -F +``` + +!!! tip Suppress logging and error output + Logging output printed to `stdout` can be suppressed by setting `--log-level=none` (see [logging](#logging)). + Output printed to `stderr` can be suppressed in different ways. To ignore all error output, add `2> /dev/null` to the end of the command line. Otherwise, specify a [reporter](#violation-reporting) to write the error output to a file. + + +### Git hooks + +Predefined git hooks can be installed, to automatically validate lint errors before commit or push. + +```shell title="Install git pre-commit hook" +ktlint installGitPreCommitHook +``` + +```shell title="Install git pre-push hook" +ktlint installGitPrePushHook +``` + +### Miscellaneous flags and commands + +`-a` or `--android`: Turn on Android Kotlin Style Guide compatibility. This flag is most likely to be removed in a future version. Use [`.editorconfig ktlint_code_style`](../../rules/configuration-ktlint/#code-style). + +`--color` and `--color-name=`: Make output colorful and optionally set the color name to use. + +`--disabled_rules=`: A comma-separated list of rules to globally disable. To disable the standard ktlint rule-set use `--disabled_rules=standard`. This flag is most likely to be removed in a future version. Use [`.editorconfig disabled_rules`](../../rules/configuration-ktlint/#disabled-rules). + +`-h` or `--help`: Prints help information. + +`--limit=`: Maximum number of errors to show (default: show all) + +`--relative`: Print files relative to the working directory (e.g. dir/file.kt instead of /home/user/project/dir/file.kt) + +`--patterns-from-stdin[=]`: Reads additional patterns from `stdin`, where the patterns are separated by ``. If `=` is omitted, newline is used as fallback delimiter. If an empty string is given, the `NUL` byte is used as delimiter instead. +If this option is given, then the default patterns are disabled. +Options `--stdin` and `--patterns-from-stdin` are mutually exclusive, only one of them can be given at a time. + +`-V` or `--version`: Prints version information and exit. + +### Microsoft Windows users + +!!! tip "Microsoft Windows" + On Microsoft Windows you'll have to use `java -jar ktlint ...`. diff --git a/documentation/snapshot/docs/install/index.md b/documentation/snapshot/docs/install/index.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/documentation/snapshot/docs/install/integrations.md b/documentation/snapshot/docs/install/integrations.md new file mode 100644 index 0000000000..76f21b8e2f --- /dev/null +++ b/documentation/snapshot/docs/install/integrations.md @@ -0,0 +1,225 @@ +## [Maven](https://github.com/shyiko/mvnw) integration + +By adding the plugin definition below to the `` section in the `pom.xml`: + +* The `ktlint` task is bound to the *Maven verify* lifecycle and will be executed each time the `mvn verify` is executed. It can also be executed with command `mvn antrun:run@ktlint`. +* The `ktlint-format` task is not bound to any other maven lifecycle. It can be executed with command `mvn antrun:run@ktlint-format`. + +See [cli usage](../cli) for arguments that can be supplied to `ktlint`. + +```xml title="Adding plugin to pom.xml" +... + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + ktlint + verify + + + + + + + + + + + run + + + + ktlint-format + + + + + + + + + + + + + run + + + + + + com.pinterest + ktlint + 0.49.0 + + + + +... +``` + +!!! Tip + If you want ktlint to run before code compilation takes place - change `verify` to `validate` (see [Maven Build Lifecycle](https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html) for more). + +!!! Info "ktlint-maven-plugin" + You might be interested to use the dedicated [gantsign/ktlint-maven-plugin](https://github.com/gantsign/ktlint-maven-plugin). + +## [Gradle](https://gradle.org/) integration + +### jlleitschuh/ktlint-gradle + +The [jlleitschuh/ktlint-gradle](https://github.com/jlleitschuh/ktlint-gradle) Gradle plugin automatically creates check and format tasks for project Kotlin sources. It supports different kotlin plugins and Gradle build caching. + +### jeremymailen/kotlinter-gradle + +The [jeremymailen/kotlinter-gradle](https://github.com/jeremymailen/kotlinter-gradle) Gradle plugin features incremental build support, file reports, and `*.kts` source support. + +### diffplug/spotless + +The [diffplug/spotless](https://github.com/diffplug/spotless/tree/master/plugin-gradle#applying-ktlint-to-kotlin-files) Gradle plugin is a general-purpose formatting plugin which amongst many others also supports `ktlint`. + +### autostyle/autostyle + +The [autostyle/autostyle](https://github.com/autostyle/autostyle/tree/master/plugin-gradle#applying-ktlint-to-kotlin-files) Gradle plugin is a general-purpose formatting plugin which amongst others also supports `ktlint`. + +### Custom Gradle integration + +#### Custom Gradle integration with Groovy + +!!! Warning + It is recommended to use one of the Gradle plugins mentioned before. + +The configuration below, defines following task: + +* The `ktlintCheck` is bound to the *Gradle check* task. It can also be executed with command `./gradlew ktlintCheck`. +* The `ktlintFormat` task is not bound to any other task. It can be executed with command `./gradlew ktlintFormat`. + +```groovy title="build.gradle" +// kotlin-gradle-plugin must be applied for configuration below to work +// (see https://kotlinlang.org/docs/reference/using-gradle.html) + +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +configurations { + ktlint +} + +dependencies { + ktlint("com.pinterest:ktlint:0.49.0") { + attributes { + attribute(Bundling.BUNDLING_ATTRIBUTE, getObjects().named(Bundling, Bundling.EXTERNAL)) + } + } + // additional 3rd party ruleset(s) can be specified here + // just add them to the classpath (e.g. ktlint 'groupId:artifactId:version') and + // ktlint will pick them up +} + +tasks.register("ktlintCheck", JavaExec) { + group = "verification" + description = "Check Kotlin code style." + classpath = configurations.ktlint + mainClass = "com.pinterest.ktlint.Main" + // see https://pinterest.github.io/ktlint/install/cli/#command-line-usage for more information + args "src/**/*.kt", "**.kts", "!**/build/**" +} + +tasks.named("check") { + dependsOn tasks.named("ktlintCheck") +} + +tasks.register("ktlintFormat", JavaExec) { + group = "formatting" + description = "Fix Kotlin code style deviations." + classpath = configurations.ktlint + mainClass = "com.pinterest.ktlint.Main" + jvmArgs "--add-opens=java.base/java.lang=ALL-UNNAMED" + // see https://pinterest.github.io/ktlint/install/cli/#command-line-usage for more information + args "-F", "src/**/*.kt", "**.kts", "!**/build/**" +} +``` + +See [Making your Gradle tasks incremental](https://proandroiddev.com/making-your-gradle-tasks-incremental-7f26e4ef09c3) by [Niklas Baudy](https://github.com/vanniktech) on how to make tasks above incremental. + +#### Custom Gradle integration with Kotlin DSL + +!!! Warning + It is recommended to use one of the Gradle plugins mentioned before. + +The configuration below, defines following task: + +* The `ktlintCheck` is bound to the *Gradle check* task. It can also be executed with command `./gradlew ktlintCheck`. +* The `ktlintFormat` task is not bound to any other task. It can be executed with command `./gradlew ktlintFormat`. + +```kotlin title="build.gradle.kts" +val ktlint by configurations.creating + +dependencies { + ktlint("com.pinterest:ktlint:0.49.0") { + attributes { + attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL)) + } + } + // ktlint(project(":custom-ktlint-ruleset")) // in case of custom ruleset +} + +val ktlintCheck by tasks.registering(JavaExec::class) { + group = LifecycleBasePlugin.VERIFICATION_GROUP + description = "Check Kotlin code style" + classpath = ktlint + mainClass.set("com.pinterest.ktlint.Main") + // see https://pinterest.github.io/ktlint/install/cli/#command-line-usage for more information + args( + "**/src/**/*.kt", + "**.kts", + "!**/build/**", + ) +} + +tasks.check { + dependsOn(ktlintCheck) +} + +tasks.register("ktlintFormat") { + group = LifecycleBasePlugin.VERIFICATION_GROUP + description = "Check Kotlin code style and format" + classpath = ktlint + mainClass.set("com.pinterest.ktlint.Main") + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + // see https://pinterest.github.io/ktlint/install/cli/#command-line-usage for more information + args( + "-F", + "**/src/**/*.kt", + "**.kts", + "!**/build/**", + ) +} +``` + +## [GNU Emacs](https://www.gnu.org/software/emacs/) integration + +See [whirm/flycheck-kotlin](https://github.com/whirm/flycheck-kotlin). + +## [Vim](https://www.vim.org/) integration + +See [w0rp/ale](https://github.com/w0rp/ale). + +## [Mega-Linter](https://nvuillam.github.io/mega-linter/) integration + +The [Mega-Linter](https://nvuillam.github.io/mega-linter/) integrates 70+ linters in a single tool for CI, including **ktlint** activated out of the box + +## Other integration + +Do you know any other integration with `ktlint` then please create a PR to add this integration to our documentation. diff --git a/documentation/snapshot/docs/install/overview.md b/documentation/snapshot/docs/install/overview.md new file mode 100644 index 0000000000..ac1b7a343b --- /dev/null +++ b/documentation/snapshot/docs/install/overview.md @@ -0,0 +1,6 @@ +See [command line interface](cli.md) or [integrations](integrations.md) for details on installing the latest release of `ktlint`. + +## Online demo + +See [`ktlint` online](https://saveourtool.com/#/demo/diktat) if you want to try-out 'ktlint'. This online version compares rule sets provided by `ktlint` and `diktat` (a layer on top of `ktlint`). To contribute to or get more info about `ktlint` online, please visit the [GitHub repository](https://github.com/saveourtool/diktat-demo). + diff --git a/documentation/snapshot/docs/install/snapshot-build.md b/documentation/snapshot/docs/install/snapshot-build.md new file mode 100644 index 0000000000..cbd594486d --- /dev/null +++ b/documentation/snapshot/docs/install/snapshot-build.md @@ -0,0 +1,37 @@ +## Access to the latest `master` snapshot + +Whenever a commit is added to the `master` branch a snapshot build is automatically uploaded to [Sonatype's snapshots repository](https://oss.sonatype.org/content/repositories/snapshots/com/pinterest/ktlint/). +If you are eager to try upcoming changes (that might or might not be included in the next stable release) you can do +so by changing version of ktlint to `-SNAPSHOT` + adding a repo: + +### Maven + +```xml +... + + sonatype-snapshots + https://oss.sonatype.org/content/repositories/snapshots + + true + + + false + + +... +``` + +### Gradle + +```groovy +repositories { + maven { + url "https://oss.sonatype.org/content/repositories/snapshots" + } +} +``` + +### Kotlin development version snapshot + +Additionally, project publishes snapshots build against latest kotlin development version. To use them, change version +of ktlint to `-kotlin-dev-SNAPSHOT`. diff --git a/documentation/snapshot/docs/readme.md b/documentation/snapshot/docs/readme.md new file mode 100644 index 0000000000..8c710bb188 --- /dev/null +++ b/documentation/snapshot/docs/readme.md @@ -0,0 +1,32 @@ +# Build & test documentation on local machine + +The documentation of ktlint is served with [mkdocs-material](https://squidfunk.github.io/mkdocs-material/creating-your-site/#advanced-configuration). For full documentation visit [mkdocs.org](https://www.mkdocs.org). + +To build and test documentation on your local development machine, follow steps below: + +## Setup +1. In IntelliJ IDEA + * Open `Preferences` + * Search for `JSON Schema mappings` + * Add new schema for url `https://squidfunk.github.io/mkdocs-material/schema.json` and add file `mkdocs.yml` for this url. +2. Pull docker image + ```shell + $ docker pull squidfunk/mkdocs-material + ``` + +## Build server +The following steps build and host the documentation locally, updating automatically whenever a local file is changed. + +1. Start mkdocs server from root of project (e.g. from same directory where file mkdocs.yml is located) + ```shell + docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material + ``` +2. Visit page `http://0.0.0.0:8000/` in your browser. +3. Edit the documentation and explicitly save the file. The mkdocs server refreshes its cached and the current page in the browser is automatically refreshed. + +## Build once +If you do not want to run a local server, or if you want to inspect the built files, you can run the following command from the project's main directory to build the documentation in the `site/` directory. + +```shell +docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material build +``` diff --git a/documentation/snapshot/docs/rules/code-styles.md b/documentation/snapshot/docs/rules/code-styles.md new file mode 100644 index 0000000000..bf41a58463 --- /dev/null +++ b/documentation/snapshot/docs/rules/code-styles.md @@ -0,0 +1,17 @@ +In ktlint `0.49` the new code style `ktlint_official` is introduced. This code style is work in progress but will become the default code style in the `1.0` release. Please try out the new code style and provide your feedback via the [issue tracker](https://github.com/pinterest/ktlint/issues). + +```ini +[*.{kt,kts}] +ktlint_code_style = ktlint_official +``` + +This `ktlint_official` code style combines the best elements from the [Kotlin Coding conventions](https://kotlinlang.org/docs/coding-conventions.html) and [Android's Kotlin styleguide](https://developer.android.com/kotlin/style-guide). This code style also provides additional formatting on topics which are not (explicitly) mentioned in those conventions and style guide. + +!!! note + Be aware that this code style in some cases formats code in a way which is not accepted by the default code formatters in IntelliJ IDEA and Android Studio. The formatters of those editors produce nicely formatted code in the vast majority of cases. But in a number of edge cases, the formatting contains bugs which are waiting to be fixed for several years. The new code style formats code in a way which is compatible with the default formatting of the editors whenever possible. When using this codestyle, it is best to disable (e.g. not use) code formatting in the editor. + +The existing code styles have been renamed to make more clear what the basis of the code style is. + +* The `official` code style has been renamed to `intellij_idea`. Code formatted with this code style aims to be compatible with default formatter of IntelliJ IDEA. This code style is based on [Kotlin Coding conventions](https://kotlinlang.org/docs/coding-conventions.html). If `.editorconfig` property `ktlint_code_style` has been set to `official` then do not forget to change the value of that property to `intellij_idea`. When not set, this is still the default code style of ktlint `0.49`. Be aware that the default code style will be changed to `ktlint_official` in the `1.0` release. + +* Code style `android` has been renamed to `android_studio`. Code formatted with this code style aims to be compatible with default formatter of Android Studio. This code style is based on [Android's Kotlin styleguide](https://developer.android.com/kotlin/style-guide). If `.editorconfig` property `ktlint_code_style` has been set to `android` then do not forget to change the value of that property to `android_studio`. diff --git a/documentation/snapshot/docs/rules/configuration-intellij-idea.md b/documentation/snapshot/docs/rules/configuration-intellij-idea.md new file mode 100644 index 0000000000..a7c570cfa4 --- /dev/null +++ b/documentation/snapshot/docs/rules/configuration-intellij-idea.md @@ -0,0 +1,36 @@ +!!! Warning + `ktlint` strives to prevent code formatting conflicts with IntelliJ IDEA / Android Studio. We recommend using either IDE formatting or `ktlint` formatting. However, if you persist on using both, then please ensure that the formatting settings are aligned as described below. This reduces the chance that code which is formatted by ktlint conflicts with formatting by the IntelliJ IDEA built-in formatter. + +!!! Note + IntelliJ IDEA supports the [kotlin coding conventions](https://kotlinlang.org/docs/coding-conventions.html). As of version 0.47.x of ktlint, the support to overwrite some configuration files of IntelliJ IDEA has been dropped as it no longer fits the scope of the project. + + +Steps: + +1. Go to your project directory +2. Create or replace file `.idea/codeStyles/codeStyleConfig.xml` with content below: + ```xml + + + + + ``` +3. Create or replace file `.idea/codeStyles/Project.xml` with content below: + ```xml + + + + + + + + + + ``` diff --git a/documentation/snapshot/docs/rules/configuration-ktlint.md b/documentation/snapshot/docs/rules/configuration-ktlint.md new file mode 100644 index 0000000000..bd7533867f --- /dev/null +++ b/documentation/snapshot/docs/rules/configuration-ktlint.md @@ -0,0 +1,258 @@ +Ktlint uses a limited set of `.editorconfig` properties for additional configuration. A sensible default value is provided for each property when not explicitly defined. Properties can be overridden, provided they are specified under `[*.{kt,kts}]`. Ktlint uses some properties defined by [.editorconfig](https://editorconfig.org/), IntelliJ IDEA and custom properties. + +!!! danger + + Unfortunately [IntelliJ IDEA](https://www.jetbrains.com/idea/) has an [autoformat issue regarding `.editorconfig`](https://youtrack.jetbrains.com/issue/IDEA-242506). Due to this error an additional space is added between glob statements, resulting in `[*{kt, kts}]` instead of `[*{kt,kts}]`. The `.editorconfig` library used by `ktlint` [ignores sections after encountering a space in the list](https://github.com/editorconfig/editorconfig/issues/148). As a result, the rule is not applied on all files as documented in the [original ktlint issue](https://github.com/pinterest/ktlint/issues/762). + +## Code style + +By default, the `intellij_idea` Kotlin code style is applied. Alternatively, the code style can be set to `ktlint_official` or `android`. + +```ini +[*.{kt,kts}] +ktlint_code_style = ktlint_official +``` + +!!! note + The default code style will be changed to `ktlint_official` in the `1.0` version of ktlint. + +## Disabled rules + +!!! note + Support of properties `disabled_rules` and `ktlint_disabled_rules` has been removed in KtLint `0.49`. + +Rule sets and individual rules can be disabled / enabled with a separate property per rule (set). + +All rules in a rule set can be enabled or disabled with a rule set property. The name of the rule set property consists of the `ktlint_` prefix followed by the rule set id. Examples: +```editorconfig +ktlint_standard = disabled # Disable all rules from the `standard` rule set provided by KtLint +ktlint_experimental = enabled # Enable all rules from the `experimental` rule set provided by KtLint +ktlint_custom-rule-set = enabled # Enable all rules in the `custom-rule-set` rule set (not provided by KtLint) +``` + +Rules that are marked as experimental will not be run, unless explicitly enabled: +```editorconfig +ktlint_experimental = enabled # Enable rules marked as experimental for all rule sets that are enabled +``` + +An individual rule can be enabled or disabled with a rule property. The name of the rule property consists of the `ktlint_` prefix followed by the rule set id followed by a `_` and the rule id. Examples: +```editorconfig +ktlint_standard_final-newline = disabled # Disables the `final-newline` rule provided by KtLint +ktlint_standard_some-experimental-rule = enabled # Enables the (experimental) `some-experimental-rule` in the `standard` rule set provided by KtLint +ktlint_custom-rule-set_custom-rule = disabled # Disables the `custom-rule` rule in the `custom-rule-set` rule set (not provided by KtLint) +``` + +!!! note + The *rule* properties are applied after applying the *rule set* properties and take precedence. So if a rule set is disabled but a specific rule of that rule set is enabled, then the rule will be executed. + + +## Final newline + +By default, a final newline is required at the end of the file. + +```ini +[*.{kt,kts}] +insert_final_newline = true +``` + +This setting only takes effect when rule `final-newline` is enabled. + +## Force multiline function signature based on number of parameters + +Setting `ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than` forces a multiline function signature in case the function contains the specified minimum number of parameters even in case the function signature would fit on a single line. Use value `unset` (default) to disable this setting. + +!!! note + By default, the `ktlint_official` code style wraps parameters of functions having at least 2 parameters. For other code styles, this setting is disabled by default. + +```ini +[*.{kt,kts}] +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than=unset +``` + +This setting only takes effect when rule `function-signature` is enabled. + +## Wrapping the expression body of a function + +Setting `ktlint_function_signature_body_expression_wrapping` determines if and when the expression body of a function is wrapped to a new line. This setting can be set to value `default`, `multiline` or `always`. + +When set to `default`, the first line of a body expression is appended to the function signature as long as the max line length is not exceeded. + +```kotlin title="ktlint_function_signature_body_expression_wrapping=default (or when not set)" +// Given that the function signature has to be written as a single line function signature +fun someFunction(a: Any, b: Any): String = "some-result" + .uppercase() + +// Given that the function signature has to be written as a multiline function signature +fun someFunction( + a: Any, + b: Any +): String = "some-result" + .uppercase() +``` + +When set to `multiline`, the body expression starts on a separate line in case it is a multiline expression. A single line body expression is wrapped only when it does not fit on the same line as the function signature. + +```kotlin title="ktlint_function_signature_body_expression_wrapping=multiline" +// Given a single line body expression and +// a the function signature that has to be written as a single line function signature and +// it does not exceed the max line length +fun someFunction(a: Any, b: Any): String = "some-result".uppercase() + +// Given a single line body expression and +// a the function signature that has to be written as a multiline function signature and +// it does not exceed the max line length +fun someFunction( + a: Any, + b: Any +): String = "some-result".uppercase() + +// Given a single line body expression then always wrap it to a separate line +fun someFunction(a: Any, b: Any): String = + "some-result" + .uppercase() +fun someFunction( + a: Any, + b: Any +): String = + "some-result" + .uppercase() +``` + +When set to `always` the body expression is always wrapped to a separate line. + +```kotlin title="ktlint_function_signature_body_expression_wrapping=always" +fun someFunction(a: Any, b: Any): String = + "some-result".uppercase() +fun functionWithAVeryLongName( + a: Any, + b: Any +): String = + "some-result" + .uppercase() +``` + +This setting only takes effect when rule `function-signature` is enabled. + +## Ignore identifiers enclosed in backticks + +By default, the identifiers enclosed in backticks are not ignored. + +According to [Kotlin coding conventions](https://kotlinlang.org/docs/reference/coding-conventions.html#names-for-test-methods) it is acceptable to write method names in natural language. When using natural language, the description tends to be longer. This property allows lines containing an identifier between backticks to be longer than the maximum line length. (Since 0.41.0) + +```kotlin +@Test +fun `Given a test with a very loooooooooooooooooooooong test description`() { + +} +``` + +```ini +[*.{kt,kts}] +ktlint_ignore_back_ticked_identifier = false +``` + +This setting only takes effect when rule `max-line-length` is enabled. + +## Import layouts + +By default, the same imports are allowed as in IntelliJ IDEA. The import path can be a full path, e.g. "java.util.List.*" as well as wildcard path, e.g. "kotlin.**". + +The layout can be composed by the following symbols: + +* `*` - wildcard. There must be at least one entry of a single wildcard to match all other imports. Matches anything after a specified symbol/import as well. +* `|` - blank line. Supports only single blank lines between imports. No blank line is allowed in the beginning or end of the layout. +* `^` - alias import, e.g. "^android.*" will match all android alias imports, "^" will match all other alias imports. + +Examples: +```kotlin +ij_kotlin_imports_layout=* # alphabetical with capital letters before lower case letters (e.g. Z before a), no blank lines +ij_kotlin_imports_layout=*,java.**,javax.**,kotlin.**,^ # default IntelliJ IDEA style, same as alphabetical, but with "java", "javax", "kotlin" and alias imports in the end of the imports list +ij_kotlin_imports_layout=android.**,|,^org.junit.**,kotlin.io.Closeable.*,|,*,^ # custom imports layout +``` + +Wildcard imports can be allowed for specific import paths (Comma-separated list, use "**" as wildcard for package and all subpackages). This setting overrides the no-wildcard-imports rule. This setting is best be used for allowing wildcard imports from libraries like Ktor where extension functions are used in a way that creates a lot of imports. + +```ini +[*.{kt,kts}] +ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.** +``` + +This setting only takes effect when rule `no-wildcard-imports` is enabled. + +## Indent size & style + +By default, indenting is done with 4 spaces per indent level. Code style `android_studio` uses a tab per indent level. + +```ini +[*.{kt,kts}] +indent_size = 4 # possible values: number (e.g. 2), "unset" (makes ktlint ignore indentation completely) +indent_style = space # or "tab" +``` + +Those settings are used by multiple rules of which rule `indent` is the most important. + +## Max line length + +By default, the maximum line length is not set. The `android` code style sets the max line length to 100 (per Android Kotlin Style Guide). + +```ini +[*.{kt,kts}] +max_line_length = off # Use "off" to ignore max line length or a positive number to set max line length +``` + +This setting is used by multiple rules of which rule `max-line-length` is the most important. + +## Trailing comma on call site + +KtLint uses the IntelliJ IDEA `.editorconfig` property `ij_kotlin_allow_trailing_comma_on_call_site` to *enforce* the usage of the trailing comma at call site when enabled. IntelliJ IDEA uses this property to *allow* the use of trailing comma but leaves it to the developer's discretion to actually use it (or not). KtLint values *consistent* formatting more than a per-situation decision. + +!!! note + In KtLint 0.48.x the default value for using the trailing comma on call site has been changed to `true` except when codestyle `android` is used. + + Although the [Kotlin coding conventions](https://kotlinlang.org/docs/reference/coding-conventions.html#trailing-commas) leaves it to the developer's discretion to use trailing commas on the call site, it also states that usage of trailing commas has several benefits: + + * It makes version-control diffs cleaner – as all the focus is on the changed value. + * It makes it easy to add and reorder elements – there is no need to add or delete the comma if you manipulate elements. + * It simplifies code generation, for example, for object initializers. The last element can also have a comma. + +Example: +```ini +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma_on_call_site = false +``` + +This setting only takes effect when rule `trailing-comma-on-call-site` is enabled. + +## Trailing comma on declaration site + +KtLint uses the IntelliJ IDEA `.editorconfig` property `ij_kotlin_allow_trailing_comma` to *enforce* the usage of the trailing comma at declaration site when enabled. IntelliJ IDEA uses this property to *allow* the use of trailing comma but leaves it to the developer's discretion to actually use it (or not). KtLint values *consistent* formatting more than a per-situation decision. + +!!! note + In KtLint 0.48.x the default value for using the trailing comma on declaration site has been changed to `true` except when codestyle `android` is used. + + The [Kotlin coding conventions](https://kotlinlang.org/docs/reference/coding-conventions.html#trailing-commas) encourages the usage of trailing commas on the declaration site, but leaves it to the developer's discretion to use trailing commas on the call site. But next to this, it also states that usage of trailing commas has several benefits: + + * It makes version-control diffs cleaner – as all the focus is on the changed value. + * It makes it easy to add and reorder elements – there is no need to add or delete the comma if you manipulate elements. + * It simplifies code generation, for example, for object initializers. The last element can also have a comma. + +Example: +```ini +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma = false # Only used for declaration site +``` + +This setting only takes effect when rule `trailing-comma-on-declaration-site` is enabled. + +## Overriding Editorconfig properties for specific directories + +You can [override](https://editorconfig.org/#file-format-details) properties for specific directories inside your project: +```ini +[*.{kt,kts}] +ktlint_standard_import-ordering = disabled + +[api/*.{kt,kts}] +ktlint_standard_indent = disabled +``` + +Note that the `import-ordering` rule is disabled for *all* packages including the `api` sub package. Next to this the `indent` rule is disabled for the `api` package and its sub packages. diff --git a/documentation/snapshot/docs/rules/dependencies.md b/documentation/snapshot/docs/rules/dependencies.md new file mode 100644 index 0000000000..823a4675f0 --- /dev/null +++ b/documentation/snapshot/docs/rules/dependencies.md @@ -0,0 +1,3 @@ +Preferably rules run independent of each other. In some case this is however not feasible. The diagram below shows the dependencies between the rules provided by KtLint. + +![Image](../../assets/images/rule-dependencies.png) diff --git a/documentation/snapshot/docs/rules/experimental.md b/documentation/snapshot/docs/rules/experimental.md new file mode 100644 index 0000000000..61561d524b --- /dev/null +++ b/documentation/snapshot/docs/rules/experimental.md @@ -0,0 +1,797 @@ +!!! important + Experimental rules in ktlint are part of the [standard ruleset](https://github.com/pinterest/ktlint/tree/master/ktlint-ruleset-standard). Experimental rules are run only when `.editorconfig` property `ktlint_experimental` is enabled. Or, when a specific experimental rule is enabled via `.editorconfig` property `ktlint__`. + +## Discouraged comment location + +Detect discouraged comment locations (no autocorrect). + +!!! note + Kotlin allows comments to be placed almost everywhere. As this can lead to code which is hard to read, most of them will never be used in practice. Ideally each rule takes comments at all possible locations into account. Sometimes this is really hard and not worth the effort. By explicitly forbidding such comment locations, the development of those rules becomes a bit easier. + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun /* some comment */ foo(t: T) = "some-result" + + fun foo() { + if (true) + // some comment + bar() + } + ``` + +Rule id: `discouraged-comment-location` + +## Disallow empty lines at start of class body + +Detect blank lines at start of a class body. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class Foo { + val foo = "foo" + } + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + class Foo { + + val foo = "foo" + } + ``` + +Rule id: `no-empty-first-line-in-class-body` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. + +## Disallow consecutive comments + +Consecutive comments are disallowed in following cases: +- Any mix of a consecutive kdoc, a block comment or an EOL comment unless separated by a blank line in between +- Consecutive KDocs (even when separated by a blank line) +- Consecutive block comments (even when separated by a blank line) + +Consecutive EOL comments are always allowed as they are often used instead of a block comment. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // An EOL comment + // may be followed by another EOL comment + val foo = "foo" + + // Different comment types (including KDoc) may be consecutive .. + + /* + * ... but do need to be separated by a blank line ... + */ + + /** + * ... but a KDoc can not be followed by an EOL or a block comment or another KDoc + */ + fun bar() = "bar" + + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + /* + * Block comments can not be consecutive ... + */ + /* + * ... even not when separated by a new line. + */ + val bar = "bar" + + /** + * A KDoc can not be followed by a block comment or an EOL comment or another KDOC + */ + + // ... even not when separated by a new line. + ``` + +Rule id: `no-consecutive-comments` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. + +## Function signature + +Rewrites the function signature to a single line when possible (e.g. when not exceeding the `max_line_length` property) or a multiline signature otherwise. In case of function with a body expression, the body expression is placed on the same line as the function signature when not exceeding the `max_line_length` property. Optionally the function signature can be forced to be written as a multiline signature in case the function has more than a specified number of parameters (`.editorconfig` property `ktlint_function_signature_wrapping_rule_always_with_minimum_parameters`) + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + fun foooooooo( + a: Any, + b: Any, + c: Any + ): String { + // body + } + + // Assume that the last allowed character is + // at the X character on the right X + fun bar(a: Any, b: Any, c: Any): String { + // body + } + + // When wrapping of body is set to 'default'. + // Assume that the last allowed character is + // at the X character on the right X + fun f(a: Any, b: Any): String = "some-result" + .uppercase() + + // When wrapping of body is set to 'multiline' + // or 'always'. + // Assume that the last allowed character is + // at the X character on the right X + fun f(a: Any, b: Any): String = + "some-result" + .uppercase() + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + fun foooooooo(a: Any, b: Any, c: Any): String { + // body + } + + // Assume that the last allowed character is + // at the X character on the right X + fun bar( + a: Any, + b: Any, + c: Any + ): String { + // body + } + + // When wrapping of body is set to 'default'. + // Assume that the last allowed character is + // at the X character on the right X + fun f(a: Any, b: Any): String = + "some-result" + .uppercase() + + // When wrapping of body is set to 'multiline' + // or 'always'. + // Assume that the last allowed character is + // at the X character on the right X + fun f(a: Any, b: Any): String = "some-result" + .uppercase() + ``` + +Rule id: `function-signature` + +## If else bracing + +If at least one branch of an if-else statement or an if-else-if statement is wrapped between curly braces then all branches should be wrapped between braces. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo(value: int) { + if (value > 0) { + doSomething() + } else if (value < 0) { + doSomethingElse() + } else { + doSomethingElse2() + } + } + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + fun foo(value: int) { + if (value > 0) + doSomething() + else if (value < 0) { + doSomethingElse() + } else + doSomethingElse2() + } + ``` + +Rule id: `if-else-bracing` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. + +## If else wrapping + +A single line if-statement should be kept simple. It may contain no more than one else-branch. The branches may not be wrapped in a block. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foobar() { + if (true) foo() + if (true) foo() else bar() + } + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + fun foobar() { + if (true) if (false) foo() else bar() + if (true) bar() else if (false) foo() else bar() + if (true) { foo() } else bar() + if (true) bar() else { if (false) foo() else bar() } + } + ``` + +Rule id: `if-else-wrapping` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. + +## Naming + +### Function naming + +Enforce naming of function. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo() {} + fun fooBar() {} + ``` +=== "[:material-heart:](#) Ktlint Test" + + ```kotlin + @Test + fun `Some name`() {} + + @Test + fun do_something() {} + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun Foo() {} + fun Foo_Bar() {} + fun `Some name`() {} + fun do_something() {} + ``` + +!!! note + Functions in files which import a class from package `org.junit`, `org.testng` or `kotlin.test` are considered to be test functions. Functions in such classes are allowed to have underscores in the name. Or function names can be specified between backticks and do not need to adhere to the normal naming convention. + +This rule can also be suppressed with the IntelliJ IDEA inspection suppression `FunctionName`. + +Rule id: `function-naming` + +### Package naming + +Enforce naming of package. + +This rule can also be suppressed with the IntelliJ IDEA inspection suppression `PackageName`. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + package foo + package foo.foo + package foo_bar + package foo.foo_bar + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + package Foo + package foo.Foo + package `foo bar` + package foo.`foo bar` + ``` + +Rule id: `package-naming` + +### Property naming + +Enforce naming of property. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val FOO_1 = Foo() + val FOO_BAR_1 = "FOO-BAR" + + var foo1: Foo = Foo() + + class Bar { + const val FOO_2 = "foo" + const val FOO_BAR_2 = "FOO-BAR" + + val foo2 = "foo" + val fooBar2 = "foo-bar" + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val Foo1 = Foo() + val FooBar1 = "FOO-BAR" + + var FOO_1: Foo = Foo() + + class Bar { + const val foo2 = "foo" + const val fooBar2 = "FOO-BAR" + + val FOO2 = "foo" + val FOO_BAR_2 = "foo-bar" + } + ``` + +!!! note + Top level `val` properties and `const val` properties have to be written in screaming snake notation. Local `val` and `const val` are written in lower camel case. + +This rule can also be suppressed with the IntelliJ IDEA inspection suppression `PropertyName`. + +Rule id: `property-naming` + +## No single line block comments + +A single line block comment should be replaced with an EOL comment when possible. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + /* + * Some comment + */ + val foo = "foo" // Some comment + val foo = { /* no-op */ } + + /* ktlint-disable foo-rule-id bar-rule-id */ + val foo = "foo" + /* ktlint-enable foo-rule-id bar-rule-id */ + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + /* Some comment */ + val foo = "foo" /* Some comment */ + ``` + +Rule id: `no-single-line-block-comment` + +## Spacing + +### No blank lines in list + +Disallow blank lines to be used in lists before the first element, between elements, and after the last element. + +*Super type* + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class FooBar: + Foo, + Bar { + // body + } + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + class FooBar: + + Foo, + + Bar + + { + // body + } + ``` + +*Type argument list* + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foobar: FooBar< + Foo, + Bar, + > = FooBar(Foo(), Bar()) + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + val foobar: FooBar< + + Foo, + + Bar, + + > = FooBar(Foo(), Bar()) + ``` + +*Type constraint list* + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class BiAdapter( + val adapter1: A1, + val adapter2: A2 + ) : RecyclerView.Adapter() + where A1 : RecyclerView.Adapter, A1 : ComposableAdapter.ViewTypeProvider, + A2 : RecyclerView.Adapter, A2 : ComposableAdapter.ViewTypeProvider { + // body + } + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + class BiAdapter( + val adapter1: A1, + val adapter2: A2 + ) : RecyclerView.Adapter() + where + A1 : RecyclerView.Adapter, A1 : ComposableAdapter.ViewTypeProvider, + + A2 : RecyclerView.Adapter, A2 : ComposableAdapter.ViewTypeProvider + { + // body + } + ``` + +*Type parameter list* + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun < + Foo, + Bar, + > foobar() + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + fun < + + Foo, + + Bar, + + > foobar() + ``` + +*Value argument list* + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foobar = foobar( + "foo", + "bar", + ) + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foobar = foobar( + + "foo", + + "bar", + + ) + ``` + +*Value parameter list* + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foobar( + foo: String, + bar: String, + ) + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foobar( + + foo: String, + + bar: String, + + ) + ``` + +Rule id: `no-blank-line-in-list` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. + +### Parameter list spacing + +Consistent spacing inside the parameter list. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo(a: Any ) = "some-result" + fun foo() = "some-result" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo( a : Any ) = "some-result" + fun foo( + ) = "some-result" + ``` + +Rule id: `parameter-list-spacing` + +### String template indent + +Enforce consistent string template indentation for multiline string templates which are post-fixed with `.trimIndent()`. The opening and closing `"""` are placed on separate lines and the indentation of the content of the template is aligned with the `"""`. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo = + """ + line1 + line2 + """.trimIndent() + fun foo() { + // The opening """ can not be wrapped to next line as that would result in a compilation error + return """ + line1 + line2 + """.trimIndent() + } + ``` +=== "[:material-heart:](#) Disallowed" + + ```kotlin + val foo = """ + line1 + line2 + """.trimIndent() + fun foo() { + return """ + line1 + line2 + """.trimIndent() + } + ``` + +Rule id: `string-template-indent` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. + +### Try catch finally spacing + +Enforce consistent spacing in `try { .. } catch { .. } finally { .. }`. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo() = + try { + // do something + } catch (exception: Exception) { + // handle exception + } finally { + // clean up + } + ``` +=== "[:material-heart:](#) Disallowed" + + ```kotlin + fun foo1() = try { /* ... */ } catch (exception: Exception) { /* ... */ } finally { /* ... */ } + fun foo2() = + try { + // do something + } + catch (exception: Exception) { + // handle exception + } + finally { + // clean up + } + ``` + +Rule id: `try-catch-finally-spacing` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. + +### Type argument list spacing + +Spacing before and after the angle brackets of a type argument list. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val res = ArrayList() + class B : A() { + override fun x() = super.x() + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val res = ArrayList < LintError > () + class B : A< T >() { + override fun x() = super< A >.x() + } + ``` + +Rule id: `type-argument-list-spacing` + +### Type parameter list spacing + +Spacing after a type parameter list in function and class declarations. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo1(t: T) = "some-result" + fun foo2(t: T) = "some-result" + fun foo3(t: T) = "some-result" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo1(t: T) = "some-result" + fun foo2(t: T) = "some-result" + funfoo3(t: T) = "some-result" + ``` + +Rule id: `type-parameter-list-spacing` + +## Wrapping + +### Content receiver wrapping + +Wraps the content receiver list to a separate line regardless of maximum line length. If the maximum line length is configured and is exceeded, wrap the context receivers and if needed its projection types to separate lines. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // ALways wrap regardless of whether max line length is set + context(Foo) + fun fooBar() + + // Wrap each context receiver to a separate line when the + // entire context receiver list does not fit on a single line + context( + Fooooooooooooooooooo1, + Foooooooooooooooooooooooooooooo2 + ) + fun fooBar() + + // Wrap each context receiver to a separate line when the + // entire context receiver list does not fit on a single line. + // Also, wrap each of it projection types in case a context + // receiver does not fit on a single line after it has been + // wrapped. + context( + Foooooooooooooooo< + Foo, + Bar + > + ) + fun fooBar() + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + // Should be wrapped regardless of whether max line length is set + context(Foo) fun fooBar() + + // Should be wrapped when the entire context receiver list does not + // fit on a single line + context(Fooooooooooooooooooo1, Foooooooooooooooooooooooooooooo2) + fun fooBar() + + // Should be wrapped when the entire context receiver list does not + // fit on a single line. Also, it should wrap each of it projection + // type in case a context receiver does not fit on a single line + // after it has been wrapped. + context(Foooooooooooooooo) + fun fooBar() + ``` + +Rule id: `context-receiver-wrapping` + +## Enum wrapping + +An enum should be a single line, or each enum entry has to be placed on a separate line. In case the enumeration contains enum entries and declarations those are to be separated by a blank line. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + enum class Foo { A, B, C, D } + + enum class Foo { + A, + B, + C, + D, + ; + + fun foo() = "foo" + } + ``` + +=== "[:material-heart:](#) Disallowed" + + ```kotlin + enum class Foo { + A, + B, C, + D + } + + enum class Foo { + A; + fun foo() = "foo" + } + ``` + +Rule id: `enum-wrapping` + +### Multiline expression wrapping + +Multiline expression on the right hand side of an expression are forced to start on a separate line. Expressions in return statement are excluded as that would result in a compilation error. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo = + foo( + parameterName = + "The quick brown fox " + .plus("jumps ") + .plus("over the lazy dog"), + ) + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo = foo( + parameterName = "The quick brown fox " + .plus("jumps ") + .plus("over the lazy dog"), + ) + ``` + +Rule id: `multiline-expression-wrapping` + +!!! Note + This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. diff --git a/documentation/snapshot/docs/rules/index.md b/documentation/snapshot/docs/rules/index.md new file mode 100644 index 0000000000..ef8c375ea0 --- /dev/null +++ b/documentation/snapshot/docs/rules/index.md @@ -0,0 +1 @@ += Rules diff --git a/documentation/snapshot/docs/rules/standard.md b/documentation/snapshot/docs/rules/standard.md new file mode 100644 index 0000000000..3bf94be6e5 --- /dev/null +++ b/documentation/snapshot/docs/rules/standard.md @@ -0,0 +1,1424 @@ +## Annotation formatting + +Multiple annotations should be on a separate line than the annotated declaration; annotations with parameters should each be on separate lines; annotations should be followed by a space + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // A single annotation (without parameters) is allowed on same line as annotated construct + @FunctionalInterface class FooBar { + @JvmField var foo: String + @Test fun bar() {} + } + // A class or function parameter may have a single annotation with parameter(s) on the same line + class Foo(@Path("fooId") val fooId: String) + class Bar( + @NotNull("fooId") val fooId: String, + @NotNull("bar") bar: String + ) + // Multiple annotations (without parameters) are allowed on the same line + @Foo @Bar + class FooBar { + @Foo @Bar + var foo: String + @Foo @Bar + fun bar() {} + } + // An array of annotations (without parameters) is allowed on same line as annotated construct + @[Foo Bar] class FooBar2 { + @[Foo Bar] var foo: String + @[Foo Bar] fun bar() {} + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + // An annotation with parameter(s) is not allowed on same line as annotated construct + @Suppress("Unused") class FooBar { + @Suppress("Unused") var foo: String + @Suppress("Unused") fun bar() {} + } + // Multiple annotation on same line as annotated construct are not allowed + @Foo @Bar class FooBar { + @Foo @Bar var foo: String + @Foo @Bar fun bar() {} + } + ``` + +Rule-id: `annotation` + +## Argument list wrapping + +All arguments should be on the same line, or every argument should be on a separate line. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val x = f( + a, + b, + c + ) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val x = f( + a, + b, c + ) + ``` + +Rule-id: `argument-list-wrapping` + +## Block comment initial star alignment + +Lines in a block comment which (exclusive the indentation) start with a `*` should have this `*` aligned with the `*` in the opening of the block comment. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + /* + * This comment is formatted well. + */ + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + /* + * This comment is not formatted well. + */ + ``` + +Rule id: `block-comment-initial-star-alignment` + +## Chain wrapping + +When wrapping chained calls `.`, `?.` and `?:` should be placed on the next line + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo = listOf(1, 2, 3) + .filter { it > 2 }!! + .takeIf { it.count() > 100 } + ?.sum() + val foobar = foo() ?: + bar + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo = listOf(1, 2, 3). + filter { it > 2 }!!. + takeIf { it.count() > 100 }?. + sum() + val foobar = foo() + ?: bar + ``` + +Rule id: `chain-wrapping` + +## Class/object naming + +Enforce naming of class. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class Foo + class Foo1 + ``` +=== "[:material-heart:](#) Ktlint JUnit Test" + + ```kotlin + @Nested + inner class `Some descriptive class name` { + @Test + fun `Some descriptive test name`() { + // do something + } + } + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + class foo + class Foo_Bar + class `Some class in the production code` + ``` + +!!! note + Functions in files which import a class from package `org.junit.jupiter.api` are considered to be test functions and are allowed to have a name specified between backticks and do not need to adhere to the normal naming convention. Although, the [Kotlin coding conventions](https://kotlinlang.org/docs/coding-conventions.html) does not allow this explicitly for class identifiers, `ktlint` does allow it. + +This rule can also be suppressed with the IntelliJ IDEA inspection suppression `ClassName`. + +Rule id: `class-naming` + +## Enum entry + +Enum entry names should be uppercase underscore-separated names. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + enum class Bar { + FOO, + Foo, + FOO_BAR, + Foo_Bar + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + enum class Bar { + foo, + bAr, + Foo_Bar, + } + ``` + +Rule id: `enum-entry-name-case` + +## File name + +A file containing only one visible (e.g. non-private) class, and visible declarations related to that class only, should be named according to that element. The same applies if the file does not contain a visible class but exactly one type alias or one object declaration. Otherwise, the PascalCase notation should be used. + +Rule id: `filename` + +## Final newline + +Ensures consistent usage of a newline at the end of each file. + +This rule can be configured with `.editorconfig` property [`insert_final_newline`](../configuration-ktlint/#final-newline). + +Rule id: `final-newline` + +## Import ordering + +Ensures that imports are ordered consistently (see [Import Layouts](../configuration-ktlint/#import-layouts) for configuration). + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + import com.bar.Bar + import com.foo.Foo + import org.foo.bar.FooBar + import java.util.concurrent.ConcurrentHashMap + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + import com.bar.Bar + import java.util.concurrent.ConcurrentHashMap + import org.foo.bar.FooBar + import com.foo.Foo + ``` + +Rule id: `import-ordering` + +## Indentation + +Indentation formatting - respects `.editorconfig` `indent_size` with no continuation indent (see [EditorConfig](../configuration-ktlint/) section for more). + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun main() { + foobar( + a, + b, + c + ) + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun main() { + foobar( + a, + b, + c + ) + } + ``` + +!!! note + This rule handles indentation for many different language constructs which can not be summarized with a few examples. See the [unit tests](https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt) for more details. + +Rule id: `indent` + +## Max line length + +Ensures that lines do not exceed the given length of `.editorconfig` property `max_line_length` (see [EditorConfig](../configuration-ktlint/) section for more). This rule does not apply in a number of situations. For example, in the case a line exceeds the maximum line length due to a comment that disables ktlint rules then that comment is being ignored when validating the length of the line. The `.editorconfig` property `ktlint_ignore_back_ticked_identifier` can be set to ignore identifiers which are enclosed in backticks, which for example is very useful when you want to allow longer names for unit tests. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + // Lines below are accepted although the max + // line length is exceeded. + package com.toooooooooooooooooooooooooooo.long + import com.tooooooooooooooooooooooooooooo.long + val foo = + """ + fooooooooooooooooooooooooooooooooooooooooo + """ + @Test + fun `Test description which is toooooooooooo long`() { + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + val fooooooooooooooo = "fooooooooooooooooooooo" + val foooooooooooooo = "foooooooooooooooooooo" // some comment + val fooooooooooooo = + "foooooooooooooooooooooooooooooooooooooooo" + ``` + +Rule id: `max-line-length` + +## Modifier order + +Consistent order of modifiers + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + abstract class A { + protected open val v = "" + internal open suspend fun f(v: Any): Any = "" + protected lateinit var lv: String + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + abstract class A { + open protected val v = "" + open suspend internal fun f(v: Any): Any = "" + lateinit protected var lv: String + } + ``` + +Rule id: `modifier-order` + +## Multiline if-else + +Braces required for multiline if/else statements. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo = + if (true) { + return 0 + } else { + return 1 + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo = + if (true) + return 0 + else + return 1 + ``` + +Rule id: `multiline-if-else` + +## No blank lines before `}` + +No blank lines before `}`. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun main() { + fun a() { + } + fun b() + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun main() { + fun a() { + + } + fun b() + + } + ``` + +Rule id: `no-blank-line-before-rbrace` + +## No blank lines in chained method calls + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo(inputText: String) { + inputText + .lowercase(Locale.getDefault()) + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo(inputText: String) { + inputText + + .lowercase(Locale.getDefault()) + } + ``` + +Rule id: `no-blank-lines-in-chained-method-calls` + +## No consecutive blank lines + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + package com.test + + import com.test.util + + val a = "a" + + fun b() { + } + + fun c() + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + package com.test + + + import com.test.util + + + val a = "a" + + + fun b() { + } + + + fun c() + ``` + +Rule id: `no-consecutive-blank-lines` + +## No empty (`{}`) class bodies + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class C + data class DC(val v: Any) + interface I + object O + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + class C {} + data class DC(val v: Any) { } + interface I { + } + object O{} + ``` + +Rule id: `no-empty-class-body` + +## No leading empty lines in method blocks + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun bar() { + val a = 2 + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun bar() { + + val a = 2 + } + ``` + +Rule id: `no-empty-first-line-in-method-block` + +## No line break after else + +Disallows line breaks after the else keyword if that could lead to confusion, for example: + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun funA() { + if (conditionA()) { + doSomething() + } else if (conditionB()) { + doAnotherThing() + } + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun funA() { + if (conditionA()) { + doSomething() + } else + if (conditionB()) { + doAnotherThing() + } + } + ``` + +Rule id: `no-line-break-after-else` + +## No line break before assignment + +When a line is broken at an assignment (`=`) operator the break comes after the symbol. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val valA = + "" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val valA + = "" + ``` + +Rule id: `no-line-break-before-assignment` + +## No multi spaces + +Except in indentation and in KDoc's it is not allowed to have multiple consecutive spaces. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun main() { + x(1, 3) + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun main() { + x(1, 3) + } + ``` + +Rule id: `no-multi-spaces` + +## No semicolons + +No semicolons (unless used to separate multiple statements on the same line). + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo() { + bar() + + bar() + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo() { + ; + bar() + ; + + bar() + + ; + } + ``` + +Rule id: `no-semi` + +## No trailing whitespaces + +Rule id: `no-trailing-spaces` + +## No `Unit` as return type + +The `Unit` type is not allowed as return type of a function. +returns (`fun fn {}` instead of `fun fn: Unit {}`) + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun fn() {} + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun fn(): Unit {} + ``` + +Rule id: `no-unit-return` + +## No unused imports + +!!! warning + This rule is not able to detect *all* unused imports as mentioned in this [issue comment](https://github.com/pinterest/ktlint/issues/1754#issuecomment-1368201667). + +Rule id: `no-unused-imports` + +## No wildcard imports + +No wildcard imports except imports listed in `.editorconfig` property `ij_kotlin_packages_to_use_import_on_demand`. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + import foobar.Bar + import foobar.Foo + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + import foobar.* + ``` + +!!! warning + In case property `ij_kotlin_packages_to_use_import_on_demand` is not explicitly set, it allows wildcards imports like `java.util.*` by default to keep in sync with IntelliJ IDEA behavior. To disallow *all* wildcard imports, add property below to your `.editorconfig`: + ```editorconfig + [*.{kt,kts}] + ij_kotlin_packages_to_use_import_on_demand = unset + ``` + +Rule id: `no-wildcard-imports` + +## Package name + +Validates that the package name matches the regular expression `[a-z][a-zA-Z\d]*(\.[a-z][a-zA-Z\d]*)*`. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + package foo + package foo.bar + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + package Foo + package foo.Foo + package `foo bar` + package foo.`foo bar` + ``` + +Rule id: `package-name` + +## Parameter list wrapping + +When class/function signature doesn't fit on a single line, each parameter must be on a separate line + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class ClassA(paramA: String, paramB: String, paramC: String) + class ClassA( + paramA: String, + paramB: String, + paramC: String + ) + fun f(a: Any, b: Any, c: Any) + fun f( + a: Any, + b: Any, + c: Any + ) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + class ClassA( + paramA: String, paramB: String, + paramC: String + ) + fun f( + a: Any, + b: Any, c: Any + ) + ``` + +Rule id: `parameter-list-wrapping` + +## Parameter wrapping + +When a function or class parameter doesn't fit on a single line, wrap the type or value to a separate line + +=== "[:material-heart:](#) Ktlint (ktlint_official)" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + class Bar( + val fooooooooooooooooooooooooTooLong: + Foo, + ) + fun bar( + fooooooooooooooooooooooooTooLong: + Foo, + ) + ``` +=== "[:material-heart:](#) Ktlint (non ktlint_official)" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + class Bar( + val fooooooooooooooooooooooooTooLong: + Foo, + ) + fun bar( + fooooooooooooooooooooooooTooLong: + Foo, + ) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + class Bar( + val fooooooooooooooooooooooooTooLong: Foo, + ) + fun bar( + fooooooooooooooooooooooooooooTooLong: Foo, + ) + ``` + +Rule id: `parameter-wrapping` + +## Property wrapping + +When a property doesn't fit on a single line, wrap the type or value to a separate line + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + val aVariableWithALooooooooooooongName: + String + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + // Assume that the last allowed character is + // at the X character on the right X + val aVariableWithALooooooooooooongName: String + ``` + +Rule id: `property-wrapping` + +## String template + +Consistent string templates (`$v` instead of `${v}`, `${p.v}` instead of `${p.v.toString()}`) + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo = "$foo hello" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo = "${foo} hello" + ``` + +Rule id: `string-template` + +## Trailing comma on call site + +Consistent removal (default) or adding of trailing commas on call site. + +!!! important + KtLint uses the IntelliJ IDEA `.editorconfig` property `ij_kotlin_allow_trailing_comma_on_call_site` to configure the rule. When this property is enabled, KtLint *enforces* the usage of the trailing comma at call site while IntelliJ IDEA default formatter only *allows* to use the trailing comma but leaves it to the developer's discretion to actually use it (or not). KtLint values *consistent* formatting more than a per-situation decision. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + FooWrapper( + Foo( + a = 3, + b = 4, + ), + ) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + FooWrapper(Foo( + a = 3, + b = 4, + ),) // it's weird to insert "," between unwrapped (continued) parenthesis + ``` + +!!! note + In KtLint 0.48.x the default value for using the trailing comma on call site has been changed to `true` except when codestyle `android` is used. + + Although the [Kotlin coding conventions](https://kotlinlang.org/docs/reference/coding-conventions.html#trailing-commas) leaves it to the developer's discretion to use trailing commas on the call site, it also states that usage of trailing commas has several benefits: + + * It makes version-control diffs cleaner – as all the focus is on the changed value. + * It makes it easy to add and reorder elements – there is no need to add or delete the comma if you manipulate elements. + * It simplifies code generation, for example, for object initializers. The last element can also have a comma. + +!!! note + Trailing comma on call site is automatically disabled if the [Wrapping](#wrapping) rule (or, before version `0.45.0`, the [Indentation](#indentation) rule) is disabled or not loaded. Because it cannot provide proper formatting with unwrapped calls. (see [dependencies](./dependencies.md)). + +Rule id: `trailing-comma-on-call-site` + +## Trailing comma on declaration site + +Consistent removal (default) or adding of trailing commas on declaration site. + +!!! important + KtLint uses the IntelliJ IDEA `.editorconfig` property `ij_kotlin_allow_trailing_comma` to configure the rule. When this property is enabled, KtLint *enforces* the usage of the trailing comma at declaration site while IntelliJ IDEA default formatter only *allows* to use the trailing comma but leaves it to the developer's discretion to actually use it (or not). KtLint values *consistent* formatting more than a per-situation decision. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class FooWrapper( + val foo = Foo( + a = 3, + b = 4, + ), + ) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + class FooWrapper(val foo = Foo( + a = 3, + b = 4, + ),) // it's weird to insert "," between unwrapped (continued) parenthesis + ``` + +!!! note + In KtLint 0.48.x the default value for using the trailing comma on declaration site has been changed to `true` except when codestyle `android` is used. + + The [Kotlin coding conventions](https://kotlinlang.org/docs/reference/coding-conventions.html#trailing-commas) encourages the usage of trailing commas on the declaration site, but leaves it to the developer's discretion to use trailing commas on the call site. But next to this, it also states that usage of trailing commas has several benefits: + + * It makes version-control diffs cleaner – as all the focus is on the changed value. + * It makes it easy to add and reorder elements – there is no need to add or delete the comma if you manipulate elements. + * It simplifies code generation, for example, for object initializers. The last element can also have a comma. + +!!! note + Trailing comma on declaration site is automatically disabled if the [Wrapping](#wrapping) rule (or, before version `0.45.0`, the [Indentation](#indentation) rule) is disabled or not loaded. Because it cannot provide proper formatting with unwrapped declarations. (see [dependencies](./dependencies.md)). + +Rule id: `trailing-comma-on-declaration-site` + +## Unnecessary parenthesis before trailing lambda + +An empty parentheses block before a lambda is redundant. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + "some-string".count { it == '-' } + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + "some-string".count() { it == '-' } + ``` + +Rule id: `unnecessary-parentheses-before-trailing-lambda` + +## Wrapping + +### Wrapping + +Inserts missing newlines (for example between parentheses of a multi-line function call). + +Rule id: `wrapping` + +### Comment wrapping + +A block comment should start and end on a line that does not contain any other element. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + /* Some comment 1 */ + val foo1 = "foo1" + val foo2 = "foo" // Some comment + val foo3 = { /* no-op */ } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + /* Some comment 1 */ val foo1 = "foo1" + val foo2 = "foo" /* Block comment instead of end-of-line comment */ + val foo3 = "foo" /* Some comment + * with a newline + */ + ``` + +Rule id: `comment-wrapping` + +## Spacing + +### Angle bracket spacing + +No spaces around angle brackets when used for typing. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val a: Map = mapOf() + val b: Map = mapOf() + val c: Map = mapOf() + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val a: Map< Int, String> = mapOf() + val b: Map = mapOf() + val c: Map = mapOf() + ``` + +Rule id: `spacing-around-angle-brackets` + +### Annotation spacing + +Annotations should be separated by a single line break. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + @JvmField + fun foo() {} + + /** + * block comment + */ + @Foo @Bar + class FooBar { + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + @JvmField + + fun foo() {} + + @Foo @Bar + /** + * block comment + */ + class FooBar { + } + ``` + +Rule id: `annotation-spacing` + +### Blank line between declarations with annotations + +Declarations with annotations should be separated by a blank line. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun a() + + @Bar + fun b() + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun a() + @Bar + fun b() + ``` + +Rule id: `spacing-between-declarations-with-annotations` + +### Blank line between declaration with comments + +Declarations with comments should be separated by a blank line. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // some comment 1 + bar() + + /* + * some comment 2 + */ + foo() + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + // some comment 1 + bar() + /* + * some comment 2 + */ + foo() + ``` + +Rule id: `spacing-between-declarations-with-comments` + +### Colon spacing + +Consistent spacing around colon. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class A : B + class A2 : B2 + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + class A:B + class A2 : B2 + ``` + +Rule id: `colon-spacing` + +### Comma spacing + +Consistent spacing around comma. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo1 = Foo(1, 3) + val foo2 = Foo(1, 3) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo1 = Foo(1 ,3) + val foo2 = Foo(1,3) + ``` + +Rule id: `comma-spacing` + +### Comment spacing + +The end of line comment sign `//` should be preceded and followed by exactly a space. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + // comment + var debugging = false // comment + var debugging = false // comment + var debugging = false // comment + fun main() { + System.out.println( // 123 + "test" + ) + } + // comment + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + //comment + var debugging = false// comment + var debugging = false //comment + var debugging = false//comment + fun main() { + System.out.println(//123 + "test" + ) + } + //comment + ``` + +Rule id: `comment-spacing` + +### Curly spacing + +Consistent spacing around curly braces. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo = if (true) { 0 } else { 1 } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo = if (true){0}else{1} + ``` + +Rule id: `curly-spacing` + +### Dot spacing + +Consistent spacing around dots. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun String.foo() = "foo" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun String . foo() = "foo" + ``` + +Rule id: `dot-spacing` + +### Double colon spacing + +No spaces around `::`. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo = Foo::class + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo1 = Foo ::class + val foo2 = Foo:: class + val foo3 = Foo :: class + val foo4 = Foo:: + class + ``` + +Rule id: `double-colon-spacing` + +### Function return type spacing + +Consistent spacing around the function return type. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo(): String = "some-result" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo1() : String = "some-result" + fun foo2(): String = "some-result" + fun foo3():String = "some-result" + fun foo4(): + String = "some-result" + ``` + +Rule id: `function-return-type-spacing` + +### Function start of body spacing + +Consistent spacing before start of function body. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo1() = "some-result" + fun foo2() = + "some-result" + fun foo3() { + // do something + } + fun bar1(): String = "some-result" + fun bar2(): String = + "some-result" + fun bar3(): String { + return "some-result" + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo1()= "some-result" + fun foo2() + = "some-result" + fun foo3() + { + // do something + } + fun bar1(): String= "some-result" + fun bar2(): String + = "some-result" + fun bar3(): String + { + return "some-result" + } + ``` + +Rule id: `function-start-of-body-spacing`: + +### Function type reference spacing + +Consistent spacing in the type reference before a function. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun String.foo() = "some-result" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun String .foo() = "some-result" + fun String + .foo() = "some-result" + fun String? .foo() = "some-result" + fun String? + .foo() = "some-result" + ``` + +Rule id: `function-type-reference-spacing` + +### Fun keyword spacing + +Consistent spacing after the fun keyword. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo() = "some-result" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo() = "some-result" + fun + foo() = "some-result" + ``` + +Rule id: `fun-keyword-spacing` + +### Kdoc wrapping + +A KDoc comment should start and end on a line that does not contain any other element. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + /** Some KDoc comment 1 */ + val foo1 = "foo1" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + /** Some KDoc comment 1 */ val foo1 = "foo1" + val foo2 = "foo2" /** Some KDoc comment + * with a newline + */ + ``` + +Rule id: `kdoc-wrapping` + +### Keyword spacing + +Consistent spacing around keywords. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun main() { + if (true) {} + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun main() { + if(true){} + } + ``` + +Rule id: `keyword-spacing` + +### Modifier list spacing + +Consistent spacing between modifiers in and after the last modifier in a modifier list. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + abstract class Foo { + protected abstract suspend fun execute() + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + abstract class Foo { + protected abstract suspend fun execute() + } + abstract + class Foo { + protected + abstract + suspend + fun execute() + } + ``` + +Rule id: `modifier-list-spacing` + +### Nullable type spacing + +No spaces in a nullable type. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo: String? = null + val foo: List = listOf(null) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo: String ? = null + val foo: List = listOf(null) + ``` + +Rule id: `nullable-type-spacing` + +### Operator spacing + +Consistent spacing around operators. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo1 = 1 + 2 + val foo2 = 1 - 2 + val foo3 = 1 * 2 + val foo4 = 1 / 2 + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo1 = 1+2 + val foo2 = 1- 2 + val foo3 = 1 *2 + val foo4 = 1 / 2 + ``` + +Rule id: `op-spacing` + +### Parenthesis spacing + +Consistent spacing around parenthesis. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class Foo : Bar { + constructor(string: String) : super() + } + val foo1 = ((1 + 2) / 3) + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + class Foo : Bar { + constructor(string: String) : super () + } + val foo1 = ( (1 + 2 ) / 3) + ``` + +Rule id: `paren-spacing` + +### Range spacing + +Consistent spacing around range operators. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo1 = (1..12 step 2).last + val foo2 = (1..12 step 2).last + val foo3 = (1..12 step 2).last + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo1 = (1.. 12 step 2).last + val foo2 = (1 .. 12 step 2).last + val foo3 = (1 ..12 step 2).last + ``` + +Rule id: `range-spacing` + +### Spacing between function name and opening parenthesis + +Consistent spacing between function name and opening parenthesis. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo() = "foo" + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo () = "foo" + ``` + +Rule id: `spacing-between-function-name-and-opening-parenthesis` + +### Unary operator spacing + +No spaces around unary operators. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo1(i: Int) = i++ + fun foo2(i: Int) = ++i + fun foo3(i: Int) = ++i + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo1(i: Int) = i ++ + fun foo2(i: Int) = ++ i + fun foo3(i: Int) = ++ + i + ``` + +Rule id: `unary-op-spacing` diff --git a/documentation/snapshot/mkdocs.yml b/documentation/snapshot/mkdocs.yml new file mode 100644 index 0000000000..ee9f9843b0 --- /dev/null +++ b/documentation/snapshot/mkdocs.yml @@ -0,0 +1,90 @@ +site_name: Ktlint +site_url: https://pinterest.github.io/ktlint/ + +site_dir: build/site +docs_dir: docs + +extra: + version: + provider: mike + +theme: + name: material + custom_dir: overrides + favicon: assets/images/favicon.ico + palette: + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: pink + toggle: + icon: material/brightness-7 + name: Switch to dark mode + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: pink + toggle: + icon: material/brightness-4 + name: Switch to light mode + icon: + repo: material/github + features: + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.expand + - search.suggest + - search.share + +nav: + - Home: index.md + - Installation: + - Overview: install/overview.md + - Command line: install/cli.md + - Integrations: install/integrations.md +# - API: install/api.md TODO: properly document + - Snapshot build: install/snapshot-build.md + - Rules: + - Code styles: rules/code-styles.md + - Standard rules: rules/standard.md + - Experimental rules: rules/experimental.md + - Dependencies: rules/dependencies.md + - KtLint configuration: rules/configuration-ktlint.md + - IntelliJ IDEA configuration: rules/configuration-intellij-idea.md + - API: + - Overview: api/overview.md + - Custom integration: api/custom-integration.md + - Custom rule set: api/custom-rule-set.md + - Custom reporter: api/custom-reporter.md + - Badge: api/badge.md + - FAQ: faq.md + - Contributing: + - Overview: contributing/overview.md + - Guidelines: contributing/guidelines.md + - Code of conduct: contributing/code-of-conduct.md + +plugins: + - search + +repo_url: https://github.com/pinterest/ktlint +repo_name: pinterest/ktlint + +markdown_extensions: + - toc: + permalink: true + - admonition + - pymdownx.emoji: + emoji_generator: !!python/name:materialx.emoji.to_svg + emoji_index: !!python/name:materialx.emoji.twemoji + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true diff --git a/documentation/snapshot/overrides/main.html b/documentation/snapshot/overrides/main.html new file mode 100644 index 0000000000..e658abc65b --- /dev/null +++ b/documentation/snapshot/overrides/main.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + +{% block outdated %} +You're viewing the SNAPSHOT version of the documentation. Be sure to use the + + SNAPSHOT version of ktlint + +, or go to the latest + + RELEASE version of the documentation + +{% endblock %} diff --git a/documentation/snapshot/run-mkdocs-server.sh b/documentation/snapshot/run-mkdocs-server.sh new file mode 100755 index 0000000000..a4be213a57 --- /dev/null +++ b/documentation/snapshot/run-mkdocs-server.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +echo "Run mkdocs server. Terminate with CTRL-C" +echo +docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material From 0733b864998e344eb26b6c2b112632afb9f29ae8 Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Thu, 11 May 2023 17:26:31 +0200 Subject: [PATCH 43/80] Rename Github workflows and restrict publish-snapshot-build to code files (#2009) --- .../workflows/{gradle-pr-build.yml => build-pull-request.yml} | 3 +-- .github/workflows/gradle-wrapper-validation.yml | 1 + .github/workflows/{release.yml => publish-release-build.yml} | 2 +- .../workflows/{release-docs.yml => publish-release-docs.yml} | 2 ++ .../{gradle-snapshot-build.yml => publish-snapshot-build.yml} | 3 ++- .../{gradle-docs-publish.yml => publish-snapshot-docs.yml} | 2 ++ 6 files changed, 9 insertions(+), 4 deletions(-) rename .github/workflows/{gradle-pr-build.yml => build-pull-request.yml} (98%) rename .github/workflows/{release.yml => publish-release-build.yml} (99%) rename .github/workflows/{release-docs.yml => publish-release-docs.yml} (99%) rename .github/workflows/{gradle-snapshot-build.yml => publish-snapshot-build.yml} (90%) rename .github/workflows/{gradle-docs-publish.yml => publish-snapshot-docs.yml} (99%) diff --git a/.github/workflows/gradle-pr-build.yml b/.github/workflows/build-pull-request.yml similarity index 98% rename from .github/workflows/gradle-pr-build.yml rename to .github/workflows/build-pull-request.yml index f2d9169a42..594c853b94 100644 --- a/.github/workflows/gradle-pr-build.yml +++ b/.github/workflows/build-pull-request.yml @@ -1,4 +1,4 @@ -name: PR Build +name: Build pull request on: push: @@ -38,4 +38,3 @@ jobs: run: ./gradlew build ktlintCheck --no-configuration-cache - name: Build with dev Kotlin version run: ./gradlew -PkotlinDev build ktlintCheck --no-configuration-cache - diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 3f086da669..9b0767d294 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -1,4 +1,5 @@ name: "Validate Gradle Wrapper" + on: [ push, pull_request ] jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/publish-release-build.yml similarity index 99% rename from .github/workflows/release.yml rename to .github/workflows/publish-release-build.yml index c95a8cf618..16151a7a5d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/publish-release-build.yml @@ -1,4 +1,4 @@ -name: Publish Release +name: Publish release build on : push : diff --git a/.github/workflows/release-docs.yml b/.github/workflows/publish-release-docs.yml similarity index 99% rename from .github/workflows/release-docs.yml rename to .github/workflows/publish-release-docs.yml index 2f46b129b1..3f97b19d9f 100644 --- a/.github/workflows/release-docs.yml +++ b/.github/workflows/publish-release-docs.yml @@ -1,8 +1,10 @@ name: Publish release documentation + on: push: branches: ['master'] paths: ['documentation/release-latest/**'] + jobs: deploy: runs-on: ubuntu-latest diff --git a/.github/workflows/gradle-snapshot-build.yml b/.github/workflows/publish-snapshot-build.yml similarity index 90% rename from .github/workflows/gradle-snapshot-build.yml rename to .github/workflows/publish-snapshot-build.yml index c76a70c8f3..59c9add0d3 100644 --- a/.github/workflows/gradle-snapshot-build.yml +++ b/.github/workflows/publish-snapshot-build.yml @@ -1,8 +1,9 @@ -name: Snapshot Publish +name: Publish snapshot build on: push: branches: [ master ] + paths: ['**/*.kt', '**/*.kts', '**/*.properties', '**/*.toml'] env: SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} diff --git a/.github/workflows/gradle-docs-publish.yml b/.github/workflows/publish-snapshot-docs.yml similarity index 99% rename from .github/workflows/gradle-docs-publish.yml rename to .github/workflows/publish-snapshot-docs.yml index 556be25254..4c376dd3bf 100644 --- a/.github/workflows/gradle-docs-publish.yml +++ b/.github/workflows/publish-snapshot-docs.yml @@ -1,8 +1,10 @@ name: Publish snapshot documentation + on: push: branches: ['master'] paths: ['documentation/snapshot/**'] + jobs: deploy: runs-on: ubuntu-latest From 7428f84c94f759eebb079d746d872ce6e5366359 Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Thu, 11 May 2023 20:19:44 +0200 Subject: [PATCH 44/80] Documentation how to (#2012) --- documentation/readme.md | 66 +++++++++++++++++-- .../release-latest/run-mkdocs-server.sh | 5 -- .../release-latest/serve-docs-locally.sh | 12 ++++ documentation/snapshot/run-mkdocs-server.sh | 5 -- documentation/snapshot/serve-docs-locally.sh | 12 ++++ 5 files changed, 86 insertions(+), 14 deletions(-) delete mode 100755 documentation/release-latest/run-mkdocs-server.sh create mode 100755 documentation/release-latest/serve-docs-locally.sh delete mode 100755 documentation/snapshot/run-mkdocs-server.sh create mode 100755 documentation/snapshot/serve-docs-locally.sh diff --git a/documentation/readme.md b/documentation/readme.md index 6073730275..ad49a49f6d 100644 --- a/documentation/readme.md +++ b/documentation/readme.md @@ -2,14 +2,72 @@ Two versions of the documentation are kept in the 'master' branch: -* The `snapshot` version of the documentation applies to the SNAPSHOT versions of ktlint. Upon the publication of the next official release of ktlint, the `release-latest` version of the documentation is replaced with the `snapshot` version. -* The `release-latest` version of the documentation applies to the last officially published version of ktlint. Upon the publication of the next official release of ktlint, this version of the documentation is replaced with the `snapshot` version. +* The `snapshot` version of the documentation applies to the SNAPSHOT versions of ktlint. Upon the publication of the next official release of ktlint, the `release-latest` version of the documentation is replaced with the `snapshot` version. See script `.announce` which is executed by the Github workflow `publish-release-build`. +* The `release-latest` version of the documentation applies to the last officially published version of ktlint. Upon the publication of the next official release of ktlint, this version of the documentation is replaced with the `snapshot` version. See script `.announce` which is executed by the Github workflow `publish-release-build`. Whenever a fix is to be made to the documentation, it should be determined in which version(s) of the documentation is to be fixed. Documentation fixes which only apply to the `SNAPSHOT` version of ktlint may only be fixed in the `snapshot` version of the documentation. -All other kind of documentation fixes most likely needs to be fixed in both the `snapshot` and `release-latest` versions. Only fixing it in `release-latest` may result in the fix being lost upon publication of the next official ktlint version. +Documentation changes related to the latest released version, and which can not wait to be published with the documentation of the next release, need to be fixed in both the `snapshot` and `release-latest` versions. Only fixing it in `release-latest` results in the fix being lost upon publication of the next official ktlint version. Small typo's can be fixed in both, but it is also okay to only fix them in the `snapshot` only. +IMPORTANT: Fixing the `snapshot` documentation is more important than fixing the `release-latest` documentation! Try to fix the `release-latest` only when the fix is important (e.g. misleading or confusing for users). Docs can be viewed on the local machine in one of following ways: * Run script `serve-docs-locally.sh` which starts a docker container running mkdocs -* Run command `mike serve` which requires that `python`, `pip`, `mike` and `mkdocs` have been installed (one time only) before +* Run command `mike serve` which requires that `python`, `pip`, `mike` and `mkdocs` have been installed (one time only) before + +NOTE: Changes to the documentation have to be submitted as PR. When merging to `master` branch, the Github workflows `publish-snapshot-docs` and `publish-release-docs` take care of publishing the docs to Github Pages. + +## Rebuilding Github Pages manually + +Github Pages deploys the documentation from branch `gh-pages`. Each version of the documentation is stored in a separate directory of that branch. Each of those directories contains a html/css version of the generated documentation. Beside the directories for released versions, two special directories exist. The `latest` directory which is based on the `documentation/release-latest` directory on the `master` branch. The `dev-snapshot` directory is based on the `documentation/snapshot` directory on the `master` branch. + +In case the `gh-pages` branch is corrupt, it can be recreated again. In order to execute commands below, `python`, `pip`, `mkdocs` and `mike` need to be installed on the local machine. The commands have to be executed from the designated directory. Note that after each command a Github Actions workflow is started (see https://github.com/pinterest/ktlint/actions). Wait with executing the next command until the Github Action workflow is completed. In between steps, the command `mike list` can be executed to see what versions have been published. + +1) Clear all doc versions from gh-pages + ```shell + # Execute from directory `documentation/release-latest` + mike delete --all --push + ``` + After the Github Actions workflow is completed, the Github Pages will be empty, and displays a 404 error page. + +2) Recreate the documentation for the last released version: + Reset the active branch to the commit with description `Updated refs to latest (xx.yy.zz) release`. + In directory `documentation/release-latest`: + ```shell + # Execute from directory `documentation/release-latest` + mike deploy --push --update-aliases xx.yy.zz latest # Replace "xx.yy.zz" with the version number. Do not remove or alter tag "latest" + ``` + After the Github Actions workflow is completed, the documentation is available at https://pinterest.github.io/ktlint/xx.yy.zz/ but is not yet available on https://pinterest.github.io/ktlint/ until the Github Action workflow for next command is completed: + ```shell + # Execute from directory `documentation/release-latest` + ``` + mike set-default --push latest + +3) Recreate the documentation for older releases: + Reset the active branch to the commit with description `Updated refs to latest (aa.bb.cc) release`. Note that for releases prior to `0.49.0` additional changes are needed, dependent to which tag the branch is reset: + - In case the `documentation` directory does not exist, then execute the command from the root directory of the project as the `mkdocs.yml` is residing in that directory. + - Add file `documentation/release-latest/overrides/main.html` from branch `master` to the directory where the `mkdocs.yml` file resided + - Add lines below to the `mkdocs.yml`: + ```yaml + extra: + version: + provider: mike + + theme: + name: material + custom_dir: overrides + ``` + In directory `documentation/release-latest`: + ```shell + # Execute from directory `documentation/release-latest` or from root directory of project for versions prior to `0.49.0` + mike deploy --push --update-aliases aa.bb.cc # Replace "aa.bb.cc" with the version number. Do not add tag "latest"! + ``` + After the Github Actions workflow is completed, the documentation is available at https://pinterest.github.io/ktlint/aa.bb.cc/ + +4) Recreate the snapshot documentation + In directory `documentation/snapshot`: + ```shell + # Execute from directory `documentation/snapshot` + mike deploy --push --update-aliases dev-snapshot + ``` + After the Github Actions workflow is completed, the documentation is available at https://pinterest.github.io/ktlint/dev-snapshot/. diff --git a/documentation/release-latest/run-mkdocs-server.sh b/documentation/release-latest/run-mkdocs-server.sh deleted file mode 100755 index a4be213a57..0000000000 --- a/documentation/release-latest/run-mkdocs-server.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -echo "Run mkdocs server. Terminate with CTRL-C" -echo -docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material diff --git a/documentation/release-latest/serve-docs-locally.sh b/documentation/release-latest/serve-docs-locally.sh new file mode 100755 index 0000000000..f4f4522d7b --- /dev/null +++ b/documentation/release-latest/serve-docs-locally.sh @@ -0,0 +1,12 @@ +# !/bin/bash + +echo "Serving docs from directory '$(basename "${PWD##*/}")'" +echo "" + +mkdocs serve +if [[ $? -ne 0 ]]; then + echo "Invalid command. Please ensure that 'mkdocs' is installed." + echo " - If needed install python3" + echo " - If needed run 'pip install mkdocs'" + echo "Or run 'docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material'" +fi diff --git a/documentation/snapshot/run-mkdocs-server.sh b/documentation/snapshot/run-mkdocs-server.sh deleted file mode 100755 index a4be213a57..0000000000 --- a/documentation/snapshot/run-mkdocs-server.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -echo "Run mkdocs server. Terminate with CTRL-C" -echo -docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material diff --git a/documentation/snapshot/serve-docs-locally.sh b/documentation/snapshot/serve-docs-locally.sh new file mode 100755 index 0000000000..f4f4522d7b --- /dev/null +++ b/documentation/snapshot/serve-docs-locally.sh @@ -0,0 +1,12 @@ +# !/bin/bash + +echo "Serving docs from directory '$(basename "${PWD##*/}")'" +echo "" + +mkdocs serve +if [[ $? -ne 0 ]]; then + echo "Invalid command. Please ensure that 'mkdocs' is installed." + echo " - If needed install python3" + echo " - If needed run 'pip install mkdocs'" + echo "Or run 'docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material'" +fi From 0600f9330e761bffa41ccec979573cb388878b61 Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Thu, 11 May 2023 20:48:47 +0200 Subject: [PATCH 45/80] Move Baseline to ktlint-cli-reporter-baseline (#2013) The baseline reporter creates the "baseline.xml" file which is read by the Baseline class. By moving the Baseline class into the reporter module, both the producer and consumer of the "baseline.xml" are located in same module which encapsulates this functionality. Also, it makes it possible for API consumers to reuse the functionality *and* to keep in sync with the Ktlint CLI. --- CHANGELOG.md | 3 ++- ktlint-baseline/build.gradle.kts | 9 --------- ktlint-baseline/gradle.properties | 2 -- ktlint-cli-reporter-baseline/build.gradle.kts | 2 ++ .../pinterest/ktlint/cli/reporter/baseline}/Baseline.kt | 8 ++++---- ktlint-cli/build.gradle.kts | 1 - .../pinterest/ktlint/cli/internal/KtlintCommandLine.kt | 6 +++--- .../pinterest/ktlint/cli/internal/ReporterAggregator.kt | 2 +- settings.gradle.kts | 1 - 9 files changed, 12 insertions(+), 22 deletions(-) delete mode 100644 ktlint-baseline/build.gradle.kts delete mode 100644 ktlint-baseline/gradle.properties rename {ktlint-baseline/src/main/kotlin/com/pinterest/ktlint/baseline/api => ktlint-cli-reporter-baseline/src/main/kotlin/com/pinterest/ktlint/cli/reporter/baseline}/Baseline.kt (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19e228a4c3..fe43a84cf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,8 @@ This project adheres to [Semantic Versioning](https://semver.org/). * Fix directory traversal for patterns referring to paths outside of current working directory or any of it child directories ([#2002](https://github.com/pinterest/ktlint/issues/2002)) ### Changed -* Separated Baseline functionality out of `ktlint-cli` into separate `ktlint-baseline` module for API consumers + +* Moved class `Baseline` from `ktlint-cli` to `ktlint-cli-reporter-baseline` so that Baseline functionality is reusable for API Consumers. ## [0.49.0] - 2023-04-21 diff --git a/ktlint-baseline/build.gradle.kts b/ktlint-baseline/build.gradle.kts deleted file mode 100644 index 2c35a73205..0000000000 --- a/ktlint-baseline/build.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -plugins { - id("ktlint-publication-library") -} - -dependencies { - implementation(projects.ktlintLogger) - implementation(projects.ktlintRuleEngineCore) - implementation(projects.ktlintCliReporterCore) -} diff --git a/ktlint-baseline/gradle.properties b/ktlint-baseline/gradle.properties deleted file mode 100644 index d31c7c9f55..0000000000 --- a/ktlint-baseline/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -POM_NAME=ktlint-baseline -POM_ARTIFACT_ID=ktlint-baseline diff --git a/ktlint-cli-reporter-baseline/build.gradle.kts b/ktlint-cli-reporter-baseline/build.gradle.kts index 589761a8c4..9323b9374e 100644 --- a/ktlint-cli-reporter-baseline/build.gradle.kts +++ b/ktlint-cli-reporter-baseline/build.gradle.kts @@ -3,6 +3,8 @@ plugins { } dependencies { + implementation(projects.ktlintLogger) + implementation(projects.ktlintRuleEngineCore) implementation(projects.ktlintCliReporterCore) testImplementation(projects.ktlintTest) diff --git a/ktlint-baseline/src/main/kotlin/com/pinterest/ktlint/baseline/api/Baseline.kt b/ktlint-cli-reporter-baseline/src/main/kotlin/com/pinterest/ktlint/cli/reporter/baseline/Baseline.kt similarity index 96% rename from ktlint-baseline/src/main/kotlin/com/pinterest/ktlint/baseline/api/Baseline.kt rename to ktlint-cli-reporter-baseline/src/main/kotlin/com/pinterest/ktlint/cli/reporter/baseline/Baseline.kt index 0b49bef889..22d5349200 100644 --- a/ktlint-baseline/src/main/kotlin/com/pinterest/ktlint/baseline/api/Baseline.kt +++ b/ktlint-cli-reporter-baseline/src/main/kotlin/com/pinterest/ktlint/cli/reporter/baseline/Baseline.kt @@ -1,8 +1,8 @@ -package com.pinterest.ktlint.cli.api +package com.pinterest.ktlint.cli.reporter.baseline -import com.pinterest.ktlint.cli.api.Baseline.Status.INVALID -import com.pinterest.ktlint.cli.api.Baseline.Status.NOT_FOUND -import com.pinterest.ktlint.cli.api.Baseline.Status.VALID +import com.pinterest.ktlint.cli.reporter.baseline.Baseline.Status.INVALID +import com.pinterest.ktlint.cli.reporter.baseline.Baseline.Status.NOT_FOUND +import com.pinterest.ktlint.cli.reporter.baseline.Baseline.Status.VALID import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError.Status.BASELINE_IGNORED import com.pinterest.ktlint.logger.api.initKtLintKLogger diff --git a/ktlint-cli/build.gradle.kts b/ktlint-cli/build.gradle.kts index 41c9dbe0aa..04ea51bc61 100644 --- a/ktlint-cli/build.gradle.kts +++ b/ktlint-cli/build.gradle.kts @@ -20,7 +20,6 @@ tasks.shadowJar { } dependencies { - implementation(projects.ktlintBaseline) implementation(projects.ktlintCore) implementation(projects.ktlintLogger) implementation(projects.ktlintCliReporterBaseline) diff --git a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt index 36dcfb88c5..e2aaac047d 100644 --- a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt +++ b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt @@ -2,9 +2,9 @@ package com.pinterest.ktlint.cli.internal import ch.qos.logback.classic.Level import ch.qos.logback.classic.Logger -import com.pinterest.ktlint.cli.api.Baseline -import com.pinterest.ktlint.cli.api.doesNotContain -import com.pinterest.ktlint.cli.api.loadBaseline +import com.pinterest.ktlint.cli.reporter.baseline.Baseline +import com.pinterest.ktlint.cli.reporter.baseline.doesNotContain +import com.pinterest.ktlint.cli.reporter.baseline.loadBaseline import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError.Status.FORMAT_IS_AUTOCORRECTED import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError.Status.KOTLIN_PARSE_EXCEPTION diff --git a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/ReporterAggregator.kt b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/ReporterAggregator.kt index 246c8a223b..cb9cb5d7ee 100644 --- a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/ReporterAggregator.kt +++ b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/ReporterAggregator.kt @@ -1,8 +1,8 @@ package com.pinterest.ktlint.cli.internal -import com.pinterest.ktlint.cli.api.Baseline import com.pinterest.ktlint.cli.internal.ReporterAggregator.ReporterConfigurationElement.ARTIFACT import com.pinterest.ktlint.cli.internal.ReporterAggregator.ReporterConfigurationElement.OUTPUT +import com.pinterest.ktlint.cli.reporter.baseline.Baseline import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError import com.pinterest.ktlint.cli.reporter.core.api.ReporterProviderV2 import com.pinterest.ktlint.cli.reporter.core.api.ReporterV2 diff --git a/settings.gradle.kts b/settings.gradle.kts index 336cf04026..802a76d912 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,7 +34,6 @@ include( // ktlint-core module is no longer used internally in the ktlint project except for backwards compatibility ":ktlint-core", ":ktlint-api-consumer", - ":ktlint-baseline", ":ktlint-bom", ":ktlint-cli", ":ktlint-cli-reporter-baseline", From a28e8b9a3b0c593fc650f87fb2fc7958ae357caf Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Fri, 12 May 2023 17:37:31 +0200 Subject: [PATCH 46/80] Prepare release 0.49.1 (#2015) --- CHANGELOG.md | 3 ++- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe43a84cf4..6e97d89ee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). -## Unreleased +## [0.49.1] - 2023-05-12 ### Added @@ -1875,6 +1875,7 @@ set in `[*{kt,kts}]` section). ## 0.1.0 - 2016-07-27 +[0.49.0]: https://github.com/pinterest/ktlint/compare/0.49.0...0.49.1 [0.49.0]: https://github.com/pinterest/ktlint/compare/0.48.2...0.49.0 [0.48.2]: https://github.com/pinterest/ktlint/compare/0.48.1...0.48.2 [0.48.1]: https://github.com/pinterest/ktlint/compare/0.48.0...0.48.1 diff --git a/gradle.properties b/gradle.properties index 53295eaf6a..334bb18049 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=0.49.1-SNAPSHOT +VERSION_NAME=0.49.1 GROUP=com.pinterest.ktlint POM_DESCRIPTION=An anti-bikeshedding Kotlin linter with built-in formatter. From 432f609ebbbeea7c9c668f10c09aed45ec108263 Mon Sep 17 00:00:00 2001 From: Sha Sha Chu Date: Fri, 12 May 2023 09:53:09 -0700 Subject: [PATCH 47/80] Fix for SDKMan config, and temporarily disabling (#2019) * Fix for SDKMan config, and temporarily disabling * updating CHANGELOG --- .github/workflows/publish-release-build.yml | 2 +- CHANGELOG.md | 2 -- ktlint-cli/build.gradle.kts | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-release-build.yml b/.github/workflows/publish-release-build.yml index 16151a7a5d..7ecfb50de2 100644 --- a/.github/workflows/publish-release-build.yml +++ b/.github/workflows/publish-release-build.yml @@ -72,7 +72,7 @@ jobs: download-url: https://github.com/pinterest/ktlint/releases/download/${{ env.version }}/ktlint-${{ env.version }}.zip - name: Release to sdkman - if: ${{ success() }} + if: false env: SDKMAN_KEY: ${{ secrets.SDKMAN_KEY }} SDKMAN_TOKEN: ${{ secrets.SDKMAN_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e97d89ee4..2a29174f06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,6 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Added -* `ktlint` is now available on SDKMAN! - ### Removed ### Fixed diff --git a/ktlint-cli/build.gradle.kts b/ktlint-cli/build.gradle.kts index 04ea51bc61..3afaf35942 100644 --- a/ktlint-cli/build.gradle.kts +++ b/ktlint-cli/build.gradle.kts @@ -142,7 +142,7 @@ tasks.withType().configureEach { } sdkman { - val sdkmanVersion = providers.environmentVariable("SDKMAN_KEY").orElse(project.version.toString()) + val sdkmanVersion = providers.environmentVariable("SDKMAN_VERSION").orElse(project.version.toString()) candidate.set("ktlint") version.set(sdkmanVersion) url.set("https://github.com/pinterest/ktlint/releases/download/$sdkmanVersion/ktlint-$sdkmanVersion.zip") From b81bb050adbd8478813b9affff2e305fc83d7374 Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Fri, 12 May 2023 21:43:57 +0200 Subject: [PATCH 48/80] Fix announce script (#2020) --- .announce | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.announce b/.announce index fc6da86b75..9f5870eb4f 100755 --- a/.announce +++ b/.announce @@ -75,9 +75,11 @@ fi # update version number in snapshot docs echo "Updating version numbers in (snapshot) installation documentation" -# Use "sed -i '' ..." instead of "sed -i -e ..." as the latter creates a new file on OSX -sed -i '' "s/$PREVIOUS_VERSION/$VERSION/g" ${SNAPSHOT_DOCS_DIR}/docs/install/cli.md -sed -i '' "s/$PREVIOUS_VERSION/$VERSION/g" ${SNAPSHOT_DOCS_DIR}/docs/install/integrations.md +# On local machine (OSX) Use "sed -i '' ..." instead of "sed -i -e ..." as the latter creates a new file. +# On Github Action workflow the "sed -i -e ..." is required as otherwise it results in failure +# "can't read s/0.49.0/0.49.1/g: No such file or directory" +sed -i -e "s/$PREVIOUS_VERSION/$VERSION/g" ${SNAPSHOT_DOCS_DIR}/docs/install/cli.md +sed -i -e "s/$PREVIOUS_VERSION/$VERSION/g" ${SNAPSHOT_DOCS_DIR}/docs/install/integrations.md git --no-pager diff ${DOCUMENTATION_DIR} # ask for user confirmation before committing From 709c6ce53fc1722602f901354f8a0afcf6146e41 Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Fri, 12 May 2023 21:54:05 +0200 Subject: [PATCH 49/80] Fix reference to compare 0.49.0 with 0.49.1 release (#2021) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a29174f06..f3384dbaf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1873,7 +1873,7 @@ set in `[*{kt,kts}]` section). ## 0.1.0 - 2016-07-27 -[0.49.0]: https://github.com/pinterest/ktlint/compare/0.49.0...0.49.1 +[0.49.1]: https://github.com/pinterest/ktlint/compare/0.49.0...0.49.1 [0.49.0]: https://github.com/pinterest/ktlint/compare/0.48.2...0.49.0 [0.48.2]: https://github.com/pinterest/ktlint/compare/0.48.1...0.48.2 [0.48.1]: https://github.com/pinterest/ktlint/compare/0.48.0...0.48.1 From 7884b62c4bf23ec6b75757f108c860802f97ea12 Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Fri, 12 May 2023 22:09:52 +0200 Subject: [PATCH 50/80] Updated refs to latest (0.49.1) release (#2022) Co-authored-by: Ktlint Release Workflow <> --- documentation/release-latest/docs/install/cli.md | 2 +- documentation/release-latest/docs/install/integrations.md | 6 +++--- documentation/snapshot/docs/install/cli.md | 2 +- documentation/snapshot/docs/install/integrations.md | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/documentation/release-latest/docs/install/cli.md b/documentation/release-latest/docs/install/cli.md index 59fd63ecf2..69d8457e78 100644 --- a/documentation/release-latest/docs/install/cli.md +++ b/documentation/release-latest/docs/install/cli.md @@ -12,7 +12,7 @@ All releases of `ktlint` can be downloaded from the [releases](https://github.co A particular version of `ktlint` can be downloaded with next command which also changes the file to an executable in directory `/usr/local/bin`: ```sh title="Download" -curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.49.0/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/ +curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.49.1/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/ ``` !!! tip "Curl not installed or behind proxy" diff --git a/documentation/release-latest/docs/install/integrations.md b/documentation/release-latest/docs/install/integrations.md index 76f21b8e2f..854077e597 100644 --- a/documentation/release-latest/docs/install/integrations.md +++ b/documentation/release-latest/docs/install/integrations.md @@ -56,7 +56,7 @@ See [cli usage](../cli) for arguments that can be supplied to `ktlint`. com.pinterest ktlint - 0.49.0 + 0.49.1 @@ -117,7 +117,7 @@ configurations { } dependencies { - ktlint("com.pinterest:ktlint:0.49.0") { + ktlint("com.pinterest:ktlint:0.49.1") { attributes { attribute(Bundling.BUNDLING_ATTRIBUTE, getObjects().named(Bundling, Bundling.EXTERNAL)) } @@ -167,7 +167,7 @@ The configuration below, defines following task: val ktlint by configurations.creating dependencies { - ktlint("com.pinterest:ktlint:0.49.0") { + ktlint("com.pinterest:ktlint:0.49.1") { attributes { attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL)) } diff --git a/documentation/snapshot/docs/install/cli.md b/documentation/snapshot/docs/install/cli.md index 59fd63ecf2..69d8457e78 100644 --- a/documentation/snapshot/docs/install/cli.md +++ b/documentation/snapshot/docs/install/cli.md @@ -12,7 +12,7 @@ All releases of `ktlint` can be downloaded from the [releases](https://github.co A particular version of `ktlint` can be downloaded with next command which also changes the file to an executable in directory `/usr/local/bin`: ```sh title="Download" -curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.49.0/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/ +curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.49.1/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/ ``` !!! tip "Curl not installed or behind proxy" diff --git a/documentation/snapshot/docs/install/integrations.md b/documentation/snapshot/docs/install/integrations.md index 76f21b8e2f..854077e597 100644 --- a/documentation/snapshot/docs/install/integrations.md +++ b/documentation/snapshot/docs/install/integrations.md @@ -56,7 +56,7 @@ See [cli usage](../cli) for arguments that can be supplied to `ktlint`. com.pinterest ktlint - 0.49.0 + 0.49.1 @@ -117,7 +117,7 @@ configurations { } dependencies { - ktlint("com.pinterest:ktlint:0.49.0") { + ktlint("com.pinterest:ktlint:0.49.1") { attributes { attribute(Bundling.BUNDLING_ATTRIBUTE, getObjects().named(Bundling, Bundling.EXTERNAL)) } @@ -167,7 +167,7 @@ The configuration below, defines following task: val ktlint by configurations.creating dependencies { - ktlint("com.pinterest:ktlint:0.49.0") { + ktlint("com.pinterest:ktlint:0.49.1") { attributes { attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL)) } From 48ffc6e2803b07bedb23b584053c117f203d4904 Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Fri, 12 May 2023 22:19:32 +0200 Subject: [PATCH 51/80] Prepare 0.49.2-SNAPSHOT (#2023) --- CHANGELOG.md | 10 ++++++++++ gradle.properties | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3384dbaf0..b971418b14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). +## Unreleased + +### Added + +### Removed + +### Fixed + +### Changed + ## [0.49.1] - 2023-05-12 ### Added diff --git a/gradle.properties b/gradle.properties index 334bb18049..56bca06be2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=0.49.1 +VERSION_NAME=0.49.2-SNAPSHOT GROUP=com.pinterest.ktlint POM_DESCRIPTION=An anti-bikeshedding Kotlin linter with built-in formatter. From 18a7b40d304d0040f0ce176cbbeb5804b9300cd8 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Mon, 17 Apr 2023 09:25:51 +0900 Subject: [PATCH 52/80] Add new rule don't allow empty files --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt new file mode 100644 index 0000000000..e597ad15ff --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -0,0 +1,57 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.rule.engine.core.api.ElementType +import com.pinterest.ktlint.rule.engine.core.api.Rule +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty +import com.pinterest.ktlint.ruleset.standard.StandardRule +import org.ec4j.core.model.PropertyType +import org.jetbrains.kotlin.com.intellij.lang.ASTNode + +public class NoEmptyFileRule : + StandardRule(id = "no-empty-file", usesEditorConfigProperties = setOf(NO_EMPTY_FILE_PROPERTY)), + Rule.Experimental { + private var noEmptyFile = NO_EMPTY_FILE_PROPERTY.defaultValue + + override fun beforeFirstNode(editorConfig: EditorConfig) { + noEmptyFile = editorConfig[NO_EMPTY_FILE_PROPERTY] + } + + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + node + .takeIf { it.elementType == ElementType.FILE } + ?.takeIf { it.textLength == TEXT_LENGTH_EMPTY_FILE_CONTAINS } + ?.let { + val filePath = it.psi.containingFile.virtualFile.name + val fileName = + filePath + .replace("\\", "/") // Ensure compatibility with Windows OS + .substringAfterLast("/") + emit( + 0, + "File `$fileName` should not be empty", + false, + ) + } + } + + public companion object { + private const val TEXT_LENGTH_EMPTY_FILE_CONTAINS: Int = 0 + + private val NO_EMPTY_FILE_PROPERTY: EditorConfigProperty = + EditorConfigProperty( + type = + PropertyType.LowerCasingPropertyType( + "no_empty_file", + "", + PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, + setOf(true.toString(), false.toString()), + ), + defaultValue = true, + ) + } +} From ec92ae50d872559a0ec8aeb4f04189844547a537 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Mon, 17 Apr 2023 09:28:37 +0900 Subject: [PATCH 53/80] Set to standard rule provider --- .../ktlint/ruleset/standard/StandardRuleSetProvider.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt index 04c51c4aeb..14c6bf5879 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt @@ -39,6 +39,7 @@ import com.pinterest.ktlint.ruleset.standard.rules.NoBlankLinesInChainedMethodCa import com.pinterest.ktlint.ruleset.standard.rules.NoConsecutiveBlankLinesRule import com.pinterest.ktlint.ruleset.standard.rules.NoConsecutiveCommentsRule import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyClassBodyRule +import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyFileRule import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyFirstLineInClassBodyRule import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyFirstLineInMethodBlockRule import com.pinterest.ktlint.ruleset.standard.rules.NoLineBreakAfterElseRule @@ -120,6 +121,7 @@ public class StandardRuleSetProvider : RuleProvider { NoBlankLinesInChainedMethodCallsRule() }, RuleProvider { NoConsecutiveBlankLinesRule() }, RuleProvider { NoConsecutiveCommentsRule() }, + RuleProvider { NoEmptyFileRule() }, RuleProvider { NoEmptyClassBodyRule() }, RuleProvider { NoEmptyFirstLineInClassBodyRule() }, RuleProvider { NoEmptyFirstLineInMethodBlockRule() }, From cebcb5b0bf1d3f9f358907feea149d6188b21233 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Mon, 17 Apr 2023 10:35:36 +0900 Subject: [PATCH 54/80] Refactor editorconfig property description --- .../pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index e597ad15ff..9f1214812b 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -47,7 +47,7 @@ public class NoEmptyFileRule : type = PropertyType.LowerCasingPropertyType( "no_empty_file", - "", + "Define whether empty files are allowed", PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, setOf(true.toString(), false.toString()), ), From b819b0146a1dd27d9b51da65e6fb1e67b70474e6 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Tue, 18 Apr 2023 21:02:01 +0900 Subject: [PATCH 55/80] Add test for no empty file rule --- .../standard/rules/NoEmptyFileRuleTest.kt | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt new file mode 100644 index 0000000000..0a17727ad1 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -0,0 +1,43 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule +import org.junit.jupiter.api.Test + +class NoEmptyFileRuleTest { + private val noEmptyFileRuleAssertThat = assertThatRule { NoEmptyFileRule() } + + @Test + fun `Given not empty kotlin file then ignore the rule for this file`() { + val code = """ + package tmp + """.trimIndent() + + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .hasNoLintViolations() + } + + @Test + fun `Given an empty kotlin file then do a return lint error`() { + val fileName = "Tmp.kt" + val code = """ + + """.trimIndent() + + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/$fileName") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `$fileName` should not be empty") + } + + @Test + fun `Given an empty kotlin script file then do a return lint error`() { + val fileName = "Tmp.kts" + val code = """ + + """.trimIndent() + + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/$fileName") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `$fileName` should not be empty") + } +} From cc6efa54ed301fe39e99c9757cafb59f3e2df03f Mon Sep 17 00:00:00 2001 From: ao0000 Date: Wed, 19 Apr 2023 08:05:39 +0900 Subject: [PATCH 56/80] Add to support editorconfig settings --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 9f1214812b..4a12c822c8 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -22,21 +22,23 @@ public class NoEmptyFileRule : autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, ) { - node - .takeIf { it.elementType == ElementType.FILE } - ?.takeIf { it.textLength == TEXT_LENGTH_EMPTY_FILE_CONTAINS } - ?.let { - val filePath = it.psi.containingFile.virtualFile.name - val fileName = - filePath - .replace("\\", "/") // Ensure compatibility with Windows OS - .substringAfterLast("/") - emit( - 0, - "File `$fileName` should not be empty", - false, - ) - } + if (noEmptyFile) { + node + .takeIf { it.elementType == ElementType.FILE } + ?.takeIf { it.textLength == TEXT_LENGTH_EMPTY_FILE_CONTAINS } + ?.let { + val filePath = it.psi.containingFile.virtualFile.name + val fileName = + filePath + .replace("\\", "/") // Ensure compatibility with Windows OS + .substringAfterLast("/") + emit( + 0, + "File `$fileName` should not be empty", + false, + ) + } + } } public companion object { From a4c5fcdd07d2194ccd4f43058286a41071248c48 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Wed, 19 Apr 2023 08:07:51 +0900 Subject: [PATCH 57/80] Add test for supporting editorconfig --- .../ruleset/standard/rules/NoEmptyFileRuleTest.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index 0a17727ad1..d748518402 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -40,4 +40,16 @@ class NoEmptyFileRuleTest { .asFileWithPath("/some/path/$fileName") .hasLintViolationWithoutAutoCorrect(1, 1, "File `$fileName` should not be empty") } + + @Test + fun testLintOff() { + val code = + """ + + """.trimIndent() + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to false) + .hasNoLintViolations() + } } From 42a26f19ec632477b7bb60c8d681845d6f7a3990 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Wed, 19 Apr 2023 08:08:54 +0900 Subject: [PATCH 58/80] Fix default setting and remove rule experimental --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 8 +++---- .../standard/rules/NoEmptyFileRuleTest.kt | 22 +++++++++++-------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 4a12c822c8..5209ebfbe2 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -1,7 +1,6 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.ElementType -import com.pinterest.ktlint.rule.engine.core.api.Rule import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty import com.pinterest.ktlint.ruleset.standard.StandardRule @@ -9,8 +8,7 @@ import org.ec4j.core.model.PropertyType import org.jetbrains.kotlin.com.intellij.lang.ASTNode public class NoEmptyFileRule : - StandardRule(id = "no-empty-file", usesEditorConfigProperties = setOf(NO_EMPTY_FILE_PROPERTY)), - Rule.Experimental { + StandardRule(id = "no-empty-file", usesEditorConfigProperties = setOf(NO_EMPTY_FILE_PROPERTY)) { private var noEmptyFile = NO_EMPTY_FILE_PROPERTY.defaultValue override fun beforeFirstNode(editorConfig: EditorConfig) { @@ -44,7 +42,7 @@ public class NoEmptyFileRule : public companion object { private const val TEXT_LENGTH_EMPTY_FILE_CONTAINS: Int = 0 - private val NO_EMPTY_FILE_PROPERTY: EditorConfigProperty = + public val NO_EMPTY_FILE_PROPERTY: EditorConfigProperty = EditorConfigProperty( type = PropertyType.LowerCasingPropertyType( @@ -53,7 +51,7 @@ public class NoEmptyFileRule : PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, setOf(true.toString(), false.toString()), ), - defaultValue = true, + defaultValue = false, ) } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index d748518402..169b064ea5 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -1,5 +1,6 @@ package com.pinterest.ktlint.ruleset.standard.rules +import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyFileRule.Companion.NO_EMPTY_FILE_PROPERTY import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule import org.junit.jupiter.api.Test @@ -8,36 +9,39 @@ class NoEmptyFileRuleTest { @Test fun `Given not empty kotlin file then ignore the rule for this file`() { - val code = """ + val code = + """ package tmp - """.trimIndent() - + """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") + .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasNoLintViolations() } @Test fun `Given an empty kotlin file then do a return lint error`() { val fileName = "Tmp.kt" - val code = """ - - """.trimIndent() + val code = + """ + """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/$fileName") + .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasLintViolationWithoutAutoCorrect(1, 1, "File `$fileName` should not be empty") } @Test fun `Given an empty kotlin script file then do a return lint error`() { val fileName = "Tmp.kts" - val code = """ - - """.trimIndent() + val code = + """ + """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/$fileName") + .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasLintViolationWithoutAutoCorrect(1, 1, "File `$fileName` should not be empty") } From 55fd7c0091215490b4852e7c3a1397d264af5e56 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Wed, 19 Apr 2023 08:37:55 +0900 Subject: [PATCH 59/80] Update documents about no empty file rule --- .../release-latest/docs/rules/configuration-ktlint.md | 8 ++++++++ documentation/release-latest/docs/rules/standard.md | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/documentation/release-latest/docs/rules/configuration-ktlint.md b/documentation/release-latest/docs/rules/configuration-ktlint.md index bd7533867f..b4efe6e2af 100644 --- a/documentation/release-latest/docs/rules/configuration-ktlint.md +++ b/documentation/release-latest/docs/rules/configuration-ktlint.md @@ -256,3 +256,11 @@ ktlint_standard_indent = disabled ``` Note that the `import-ordering` rule is disabled for *all* packages including the `api` sub package. Next to this the `indent` rule is disabled for the `api` package and its sub packages. + +## No empty file + +By default, empty files are allowed to exist. You can set to allow them not to exist. +```ini +[*.{kt,kts}] + no_empty_file = true # Use "true" if empty files are not allowed +``` diff --git a/documentation/release-latest/docs/rules/standard.md b/documentation/release-latest/docs/rules/standard.md index 3bf94be6e5..6f10fc6f97 100644 --- a/documentation/release-latest/docs/rules/standard.md +++ b/documentation/release-latest/docs/rules/standard.md @@ -444,6 +444,10 @@ Rule id: `no-consecutive-blank-lines` Rule id: `no-empty-class-body` +## No empty files +Ensure that no empty Kotlin or Kotlin Script files are allowed. +This rule can be configured with `.editorconfig` property [`no_empty_file`](../configuration-ktlint/#no-empty-file) + ## No leading empty lines in method blocks === "[:material-heart:](#) Ktlint" From 5cf336405d322b3d92e0044f104acd67ec5a4f4a Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sat, 22 Apr 2023 09:03:06 +0900 Subject: [PATCH 60/80] Modify to experimental rule --- .../ktlint/ruleset/standard/rules/NoEmptyFileRule.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 5209ebfbe2..415a9ce57c 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -1,6 +1,7 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.ElementType +import com.pinterest.ktlint.rule.engine.core.api.Rule import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty import com.pinterest.ktlint.ruleset.standard.StandardRule @@ -8,7 +9,8 @@ import org.ec4j.core.model.PropertyType import org.jetbrains.kotlin.com.intellij.lang.ASTNode public class NoEmptyFileRule : - StandardRule(id = "no-empty-file", usesEditorConfigProperties = setOf(NO_EMPTY_FILE_PROPERTY)) { + StandardRule(id = "no-empty-file", usesEditorConfigProperties = setOf(NO_EMPTY_FILE_PROPERTY)), + Rule.Experimental { private var noEmptyFile = NO_EMPTY_FILE_PROPERTY.defaultValue override fun beforeFirstNode(editorConfig: EditorConfig) { From d52d4490f9093ec8373e6ffb1a7a9e0440ba130c Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sat, 22 Apr 2023 09:03:56 +0900 Subject: [PATCH 61/80] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b971418b14..60cda1c6e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -318,6 +318,7 @@ if (node.isRoot()) { } ``` * Add new experimental rule `enum-wrapping` for all code styles. An enum should either be a single line, or each enum entry should be defined on a separate line. ([#1903](https://github.com/pinterest/ktlint/issues/1903)) +* Add new experimental rule `no-empty-file` for all code styles. Kotlin and Kotlin Script empty files should not be existed. ### Removed From 0fff6ace083920ee7ed3d650946e9d167c89192a Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 08:28:43 +0900 Subject: [PATCH 62/80] Fix CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60cda1c6e2..b517609097 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). ## Unreleased +* Add new experimental rule `no-empty-file` for all code styles. Kotlin and Kotlin Scription files may not be empty. ### Added @@ -318,7 +319,6 @@ if (node.isRoot()) { } ``` * Add new experimental rule `enum-wrapping` for all code styles. An enum should either be a single line, or each enum entry should be defined on a separate line. ([#1903](https://github.com/pinterest/ktlint/issues/1903)) -* Add new experimental rule `no-empty-file` for all code styles. Kotlin and Kotlin Script empty files should not be existed. ### Removed From 96216c574fcc404b5102590e79bfa809183e25bc Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 08:32:16 +0900 Subject: [PATCH 63/80] Remove no empty files section in docs --- .../release-latest/docs/rules/configuration-ktlint.md | 8 -------- documentation/release-latest/docs/rules/standard.md | 4 ---- 2 files changed, 12 deletions(-) diff --git a/documentation/release-latest/docs/rules/configuration-ktlint.md b/documentation/release-latest/docs/rules/configuration-ktlint.md index b4efe6e2af..bd7533867f 100644 --- a/documentation/release-latest/docs/rules/configuration-ktlint.md +++ b/documentation/release-latest/docs/rules/configuration-ktlint.md @@ -256,11 +256,3 @@ ktlint_standard_indent = disabled ``` Note that the `import-ordering` rule is disabled for *all* packages including the `api` sub package. Next to this the `indent` rule is disabled for the `api` package and its sub packages. - -## No empty file - -By default, empty files are allowed to exist. You can set to allow them not to exist. -```ini -[*.{kt,kts}] - no_empty_file = true # Use "true" if empty files are not allowed -``` diff --git a/documentation/release-latest/docs/rules/standard.md b/documentation/release-latest/docs/rules/standard.md index 6f10fc6f97..3bf94be6e5 100644 --- a/documentation/release-latest/docs/rules/standard.md +++ b/documentation/release-latest/docs/rules/standard.md @@ -444,10 +444,6 @@ Rule id: `no-consecutive-blank-lines` Rule id: `no-empty-class-body` -## No empty files -Ensure that no empty Kotlin or Kotlin Script files are allowed. -This rule can be configured with `.editorconfig` property [`no_empty_file`](../configuration-ktlint/#no-empty-file) - ## No leading empty lines in method blocks === "[:material-heart:](#) Ktlint" From ffa90dd23cc39ba02b2cb0f822529e2ddec6c58f Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 08:36:33 +0900 Subject: [PATCH 64/80] Change to alphabetical order --- .../ktlint/ruleset/standard/StandardRuleSetProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt index 14c6bf5879..68f5df6817 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt @@ -121,8 +121,8 @@ public class StandardRuleSetProvider : RuleProvider { NoBlankLinesInChainedMethodCallsRule() }, RuleProvider { NoConsecutiveBlankLinesRule() }, RuleProvider { NoConsecutiveCommentsRule() }, - RuleProvider { NoEmptyFileRule() }, RuleProvider { NoEmptyClassBodyRule() }, + RuleProvider { NoEmptyFileRule() }, RuleProvider { NoEmptyFirstLineInClassBodyRule() }, RuleProvider { NoEmptyFirstLineInMethodBlockRule() }, RuleProvider { NoLineBreakAfterElseRule() }, From 5e56473971d91c8ed8fb94962a2dd1daba10d709 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 09:46:34 +0900 Subject: [PATCH 65/80] Refactor NoEmptyFileRule --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 415a9ce57c..c241fb644f 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -25,25 +25,16 @@ public class NoEmptyFileRule : if (noEmptyFile) { node .takeIf { it.elementType == ElementType.FILE } - ?.takeIf { it.textLength == TEXT_LENGTH_EMPTY_FILE_CONTAINS } + ?.takeIf { it.text.isBlank() } ?.let { val filePath = it.psi.containingFile.virtualFile.name - val fileName = - filePath - .replace("\\", "/") // Ensure compatibility with Windows OS - .substringAfterLast("/") - emit( - 0, - "File `$fileName` should not be empty", - false, - ) + .replace("\\", "/") // Ensure compatibility with Windows OS + emit(0, "File `$filePath` should not be empty", false) } } } public companion object { - private const val TEXT_LENGTH_EMPTY_FILE_CONTAINS: Int = 0 - public val NO_EMPTY_FILE_PROPERTY: EditorConfigProperty = EditorConfigProperty( type = From acf24977a24e7556536992d0032a2df5563c2d0c Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 09:49:02 +0900 Subject: [PATCH 66/80] Fix to enable no_empty_file rule on default to reduce .editorconfig --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index c241fb644f..904651ebaf 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -38,13 +38,13 @@ public class NoEmptyFileRule : public val NO_EMPTY_FILE_PROPERTY: EditorConfigProperty = EditorConfigProperty( type = - PropertyType.LowerCasingPropertyType( - "no_empty_file", - "Define whether empty files are allowed", - PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, - setOf(true.toString(), false.toString()), - ), - defaultValue = false, + PropertyType.LowerCasingPropertyType( + "no_empty_file", + "Define whether empty files are allowed", + PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, + setOf(true.toString(), false.toString()), + ), + defaultValue = true, ) } } From 6544a12981f1bdff6109812ed6a1216006b4c57c Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 10:27:31 +0900 Subject: [PATCH 67/80] Fix NoEmptyFileRuleTest --- .../standard/rules/NoEmptyFileRuleTest.kt | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index 169b064ea5..8b0f2c0d33 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -8,10 +8,13 @@ class NoEmptyFileRuleTest { private val noEmptyFileRuleAssertThat = assertThatRule { NoEmptyFileRule() } @Test - fun `Given not empty kotlin file then ignore the rule for this file`() { + fun `Given non-empty kotlin file then ignore the rule for this file`() { val code = """ package tmp + fun main(){ + println("Hello world") + } """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") @@ -21,39 +24,32 @@ class NoEmptyFileRuleTest { @Test fun `Given an empty kotlin file then do a return lint error`() { - val fileName = "Tmp.kt" - val code = - """ - - """.trimIndent() + val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) - .asFileWithPath("/some/path/$fileName") + .asFileWithPath("/some/path/Tmp.kt") .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) - .hasLintViolationWithoutAutoCorrect(1, 1, "File `$fileName` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } @Test fun `Given an empty kotlin script file then do a return lint error`() { - val fileName = "Tmp.kts" - val code = - """ - - """.trimIndent() + val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) - .asFileWithPath("/some/path/$fileName") + .asFileWithPath("/some/path/Tmp.kts") .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) - .hasLintViolationWithoutAutoCorrect(1, 1, "File `$fileName` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kts` should not be empty") } @Test - fun testLintOff() { - val code = - """ - - """.trimIndent() + fun `Given empty kotlin file when lint disable then ignore the rule for this file`() { + val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to false) .hasNoLintViolations() } + + private companion object { + private const val EMPTY_FILE = "" + } } From a09b082305113b625e4790ca38c1c1af3ef3630d Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 10:29:11 +0900 Subject: [PATCH 68/80] Add non-empty kotlin file version test --- .../ruleset/standard/rules/NoEmptyFileRuleTest.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index 8b0f2c0d33..acfa06c38d 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -49,6 +49,20 @@ class NoEmptyFileRuleTest { .hasNoLintViolations() } + @Test + fun `Given non-empty kotlin file when lint disable then ignore this file`() { + val code = """ + package tmp + fun main(){ + println("Hello world") + } + """.trimIndent() + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to false) + .hasNoLintViolations() + } + private companion object { private const val EMPTY_FILE = "" } From 4ff58618daaf6fa78ab7a95c79ad7d65edf3626d Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 23 Apr 2023 10:31:31 +0900 Subject: [PATCH 69/80] Format --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 17 +++++++++-------- .../standard/rules/NoEmptyFileRuleTest.kt | 5 +++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 904651ebaf..82c57d8430 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -27,8 +27,9 @@ public class NoEmptyFileRule : .takeIf { it.elementType == ElementType.FILE } ?.takeIf { it.text.isBlank() } ?.let { - val filePath = it.psi.containingFile.virtualFile.name - .replace("\\", "/") // Ensure compatibility with Windows OS + val filePath = + it.psi.containingFile.virtualFile.name + .replace("\\", "/") // Ensure compatibility with Windows OS emit(0, "File `$filePath` should not be empty", false) } } @@ -38,12 +39,12 @@ public class NoEmptyFileRule : public val NO_EMPTY_FILE_PROPERTY: EditorConfigProperty = EditorConfigProperty( type = - PropertyType.LowerCasingPropertyType( - "no_empty_file", - "Define whether empty files are allowed", - PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, - setOf(true.toString(), false.toString()), - ), + PropertyType.LowerCasingPropertyType( + "no_empty_file", + "Define whether empty files are allowed", + PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, + setOf(true.toString(), false.toString()), + ), defaultValue = true, ) } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index acfa06c38d..dd988e98f6 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -51,12 +51,13 @@ class NoEmptyFileRuleTest { @Test fun `Given non-empty kotlin file when lint disable then ignore this file`() { - val code = """ + val code = + """ package tmp fun main(){ println("Hello world") } - """.trimIndent() + """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to false) From 0797c77f12f3c3ebda602c2eb4f571b935744adc Mon Sep 17 00:00:00 2001 From: ao0000 Date: Wed, 26 Apr 2023 23:10:50 +0900 Subject: [PATCH 70/80] Add empty file case for package or import statements only --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 82c57d8430..fc88c3fc5b 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -2,8 +2,11 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.ElementType import com.pinterest.ktlint.rule.engine.core.api.Rule +import com.pinterest.ktlint.rule.engine.core.api.children import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty +import com.pinterest.ktlint.rule.engine.core.api.isRoot +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace import com.pinterest.ktlint.ruleset.standard.StandardRule import org.ec4j.core.model.PropertyType import org.jetbrains.kotlin.com.intellij.lang.ASTNode @@ -22,17 +25,30 @@ public class NoEmptyFileRule : autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, ) { - if (noEmptyFile) { - node - .takeIf { it.elementType == ElementType.FILE } - ?.takeIf { it.text.isBlank() } - ?.let { - val filePath = - it.psi.containingFile.virtualFile.name - .replace("\\", "/") // Ensure compatibility with Windows OS - emit(0, "File `$filePath` should not be empty", false) - } - } + if (!noEmptyFile) return + + node + .takeIf { it.isRoot() } + ?.takeIf { it.isEmptyFile() } + ?.let { + val filePath = + node.psi.containingFile.virtualFile.name + .replace("\\", "/") // Ensure compatibility with Windows OS + emit(0, "File `$filePath` should not be empty", false) + } + } + + private fun ASTNode.isEmptyFile(): Boolean { + if (text.isBlank()) return true + + return this.children() + .toList() + .filter { + !it.isWhiteSpace() && + it.elementType != ElementType.PACKAGE_DIRECTIVE && + it.elementType != ElementType.IMPORT_LIST + } + .isEmpty() } public companion object { From 79bc58b55885c8db903987aecd2467428fd7e837 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sat, 29 Apr 2023 09:52:53 +0900 Subject: [PATCH 71/80] Move comment to unreleased added section in CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b517609097..cc1e22da06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,9 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). ## Unreleased -* Add new experimental rule `no-empty-file` for all code styles. Kotlin and Kotlin Scription files may not be empty. ### Added +* Add new experimental rule `no-empty-file` for all code styles. Kotlin and Kotlin Scription files may not be empty. ### Removed From 3aaeba20bb725fdf5c5fe46542d1c14eace1143c Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sat, 29 Apr 2023 09:54:57 +0900 Subject: [PATCH 72/80] Add test case about only package and only import statement --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 3 +-- .../standard/rules/NoEmptyFileRuleTest.kt | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index fc88c3fc5b..3f46fcc0f7 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -47,8 +47,7 @@ public class NoEmptyFileRule : !it.isWhiteSpace() && it.elementType != ElementType.PACKAGE_DIRECTIVE && it.elementType != ElementType.IMPORT_LIST - } - .isEmpty() + }.isEmpty() } public companion object { diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index dd988e98f6..9726f50e9e 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -40,6 +40,32 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kts` should not be empty") } + @Test + fun `Given only package statement in kotlin file then do a return lint error`() { + val code = + """ + package path + """.trimIndent() + + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) + .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") + } + + @Test + fun `Given only import statement in kotlin file then do a return lint error`() { + val code = + """ + package path + import sample.Hello + """.trimIndent() + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) + .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") + } + @Test fun `Given empty kotlin file when lint disable then ignore the rule for this file`() { val code = EMPTY_FILE From b195e95b639492d355c9c63889189d8e53dd9b2d Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 30 Apr 2023 23:17:51 +0900 Subject: [PATCH 73/80] Delete editorconfig property --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 29 +------------------ .../standard/rules/NoEmptyFileRuleTest.kt | 18 +----------- 2 files changed, 2 insertions(+), 45 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 3f46fcc0f7..00389f9380 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -3,30 +3,17 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.ElementType import com.pinterest.ktlint.rule.engine.core.api.Rule import com.pinterest.ktlint.rule.engine.core.api.children -import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig -import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty import com.pinterest.ktlint.rule.engine.core.api.isRoot import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace import com.pinterest.ktlint.ruleset.standard.StandardRule -import org.ec4j.core.model.PropertyType import org.jetbrains.kotlin.com.intellij.lang.ASTNode -public class NoEmptyFileRule : - StandardRule(id = "no-empty-file", usesEditorConfigProperties = setOf(NO_EMPTY_FILE_PROPERTY)), - Rule.Experimental { - private var noEmptyFile = NO_EMPTY_FILE_PROPERTY.defaultValue - - override fun beforeFirstNode(editorConfig: EditorConfig) { - noEmptyFile = editorConfig[NO_EMPTY_FILE_PROPERTY] - } - +public class NoEmptyFileRule : StandardRule(id = "no-empty-file"), Rule.Experimental { override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, ) { - if (!noEmptyFile) return - node .takeIf { it.isRoot() } ?.takeIf { it.isEmptyFile() } @@ -49,18 +36,4 @@ public class NoEmptyFileRule : it.elementType != ElementType.IMPORT_LIST }.isEmpty() } - - public companion object { - public val NO_EMPTY_FILE_PROPERTY: EditorConfigProperty = - EditorConfigProperty( - type = - PropertyType.LowerCasingPropertyType( - "no_empty_file", - "Define whether empty files are allowed", - PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, - setOf(true.toString(), false.toString()), - ), - defaultValue = true, - ) - } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index 9726f50e9e..0a34c726b8 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -1,6 +1,5 @@ package com.pinterest.ktlint.ruleset.standard.rules -import com.pinterest.ktlint.ruleset.standard.rules.NoEmptyFileRule.Companion.NO_EMPTY_FILE_PROPERTY import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule import org.junit.jupiter.api.Test @@ -18,7 +17,6 @@ class NoEmptyFileRuleTest { """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasNoLintViolations() } @@ -27,7 +25,6 @@ class NoEmptyFileRuleTest { val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } @@ -36,7 +33,6 @@ class NoEmptyFileRuleTest { val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kts") - .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kts` should not be empty") } @@ -49,7 +45,6 @@ class NoEmptyFileRuleTest { noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } @@ -62,21 +57,11 @@ class NoEmptyFileRuleTest { """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to true) .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } @Test - fun `Given empty kotlin file when lint disable then ignore the rule for this file`() { - val code = EMPTY_FILE - noEmptyFileRuleAssertThat(code) - .asFileWithPath("/some/path/Tmp.kt") - .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to false) - .hasNoLintViolations() - } - - @Test - fun `Given non-empty kotlin file when lint disable then ignore this file`() { + fun `Given non-empty kotlin file then ignore this file`() { val code = """ package tmp @@ -86,7 +71,6 @@ class NoEmptyFileRuleTest { """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .withEditorConfigOverride(NO_EMPTY_FILE_PROPERTY to false) .hasNoLintViolations() } From 3e7f64f292834894c023bd4916d8def6fa097fe5 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Sun, 30 Apr 2023 23:27:00 +0900 Subject: [PATCH 74/80] Add test case about only import statement --- .../ruleset/standard/rules/NoEmptyFileRuleTest.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index 0a34c726b8..fe4fd062cd 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -50,6 +50,17 @@ class NoEmptyFileRuleTest { @Test fun `Given only import statement in kotlin file then do a return lint error`() { + val code = + """ + import sample.Hello + """.trimIndent() + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") + } + + @Test + fun `Given only package and import statements in kotlin file then do a return lint error`() { val code = """ package path From 4338af8cc2de18317ca2054ae83e2e5e9a55808d Mon Sep 17 00:00:00 2001 From: ao0000 Date: Mon, 1 May 2023 09:42:46 +0900 Subject: [PATCH 75/80] Disable test case related on Windows OS file system --- .../ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index fe4fd062cd..23f8480531 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -2,6 +2,8 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.DisabledOnOs +import org.junit.jupiter.api.condition.OS class NoEmptyFileRuleTest { private val noEmptyFileRuleAssertThat = assertThatRule { NoEmptyFileRule() } @@ -20,6 +22,7 @@ class NoEmptyFileRuleTest { .hasNoLintViolations() } + @DisabledOnOs(OS.WINDOWS) @Test fun `Given an empty kotlin file then do a return lint error`() { val code = EMPTY_FILE @@ -28,6 +31,7 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } + @DisabledOnOs(OS.WINDOWS) @Test fun `Given an empty kotlin script file then do a return lint error`() { val code = EMPTY_FILE @@ -36,6 +40,7 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kts` should not be empty") } + @DisabledOnOs(OS.WINDOWS) @Test fun `Given only package statement in kotlin file then do a return lint error`() { val code = @@ -48,6 +53,7 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } + @DisabledOnOs(OS.WINDOWS) @Test fun `Given only import statement in kotlin file then do a return lint error`() { val code = @@ -59,6 +65,7 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } + @DisabledOnOs(OS.WINDOWS) @Test fun `Given only package and import statements in kotlin file then do a return lint error`() { val code = From c8dcd0d34f3e57d8ef336391ceb63768d1499301 Mon Sep 17 00:00:00 2001 From: ao0000 Date: Mon, 1 May 2023 19:55:06 +0900 Subject: [PATCH 76/80] Revert "Disable test case related on Windows OS file system" This reverts commit e3598798742a02f70f4896fb538113e92ddc9e4f. --- .../ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index 23f8480531..fe4fd062cd 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -2,8 +2,6 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule import org.junit.jupiter.api.Test -import org.junit.jupiter.api.condition.DisabledOnOs -import org.junit.jupiter.api.condition.OS class NoEmptyFileRuleTest { private val noEmptyFileRuleAssertThat = assertThatRule { NoEmptyFileRule() } @@ -22,7 +20,6 @@ class NoEmptyFileRuleTest { .hasNoLintViolations() } - @DisabledOnOs(OS.WINDOWS) @Test fun `Given an empty kotlin file then do a return lint error`() { val code = EMPTY_FILE @@ -31,7 +28,6 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } - @DisabledOnOs(OS.WINDOWS) @Test fun `Given an empty kotlin script file then do a return lint error`() { val code = EMPTY_FILE @@ -40,7 +36,6 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kts` should not be empty") } - @DisabledOnOs(OS.WINDOWS) @Test fun `Given only package statement in kotlin file then do a return lint error`() { val code = @@ -53,7 +48,6 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } - @DisabledOnOs(OS.WINDOWS) @Test fun `Given only import statement in kotlin file then do a return lint error`() { val code = @@ -65,7 +59,6 @@ class NoEmptyFileRuleTest { .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") } - @DisabledOnOs(OS.WINDOWS) @Test fun `Given only package and import statements in kotlin file then do a return lint error`() { val code = From c487e6df621a6d952bf6726e710567030d4193ee Mon Sep 17 00:00:00 2001 From: ao0000 Date: Mon, 1 May 2023 20:13:02 +0900 Subject: [PATCH 77/80] Fix lint error to use file name --- .../ktlint/ruleset/standard/rules/NoEmptyFileRule.kt | 5 +++-- .../ruleset/standard/rules/NoEmptyFileRuleTest.kt | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index 00389f9380..a705930f9e 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -18,10 +18,11 @@ public class NoEmptyFileRule : StandardRule(id = "no-empty-file"), Rule.Experime .takeIf { it.isRoot() } ?.takeIf { it.isEmptyFile() } ?.let { - val filePath = + val fileName = node.psi.containingFile.virtualFile.name .replace("\\", "/") // Ensure compatibility with Windows OS - emit(0, "File `$filePath` should not be empty", false) + .substringAfterLast("/") + emit(0, "File `$fileName` should not be empty", false) } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index fe4fd062cd..1aa2bc6cc3 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -25,7 +25,7 @@ class NoEmptyFileRuleTest { val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `Tmp.kt` should not be empty") } @Test @@ -33,7 +33,7 @@ class NoEmptyFileRuleTest { val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kts") - .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kts` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `Tmp.kts` should not be empty") } @Test @@ -45,7 +45,7 @@ class NoEmptyFileRuleTest { noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `Tmp.kt` should not be empty") } @Test @@ -56,7 +56,7 @@ class NoEmptyFileRuleTest { """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `Tmp.kt` should not be empty") } @Test @@ -68,7 +68,7 @@ class NoEmptyFileRuleTest { """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .hasLintViolationWithoutAutoCorrect(1, 1, "File `/project/some/path/Tmp.kt` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File `Tmp.kt` should not be empty") } @Test From ed040e49cbcd006e6be844f988acdd6d7a9dc21a Mon Sep 17 00:00:00 2001 From: ao0000 Date: Fri, 5 May 2023 00:30:10 +0900 Subject: [PATCH 78/80] Fix CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc1e22da06..932038d14c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Unreleased ### Added -* Add new experimental rule `no-empty-file` for all code styles. Kotlin and Kotlin Scription files may not be empty. +* Add new experimental rule `no-empty-file` for all code styles. Kotlin file may not be empty. ### Removed From 5d446a49c64f5cc83d218c159ac26d20ca91174c Mon Sep 17 00:00:00 2001 From: paul-dingemans Date: Sun, 14 May 2023 11:27:33 +0200 Subject: [PATCH 79/80] Update changelog and experimental documentation --- CHANGELOG.md | 3 ++- documentation/snapshot/docs/rules/experimental.md | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 932038d14c..5526e69237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Unreleased ### Added -* Add new experimental rule `no-empty-file` for all code styles. Kotlin file may not be empty. + +* Add new experimental rule `no-empty-file` for all code styles. A kotlin (script) file may not be empty ([#1074](https://github.com/pinterest/ktlint/issues/1074)) ### Removed diff --git a/documentation/snapshot/docs/rules/experimental.md b/documentation/snapshot/docs/rules/experimental.md index 61561d524b..8253fbc510 100644 --- a/documentation/snapshot/docs/rules/experimental.md +++ b/documentation/snapshot/docs/rules/experimental.md @@ -345,6 +345,12 @@ This rule can also be suppressed with the IntelliJ IDEA inspection suppression ` Rule id: `property-naming` +## No empty file + +A kotlin (script) file should not be empty. It needs to contain at least one declaration. Files only contain a package and/or import statements are as of that disallowed. + +Rule id: `no-empty-file` + ## No single line block comments A single line block comment should be replaced with an EOL comment when possible. From d62ff7e07a1bc52650777fd355c4b2caaaed0c3d Mon Sep 17 00:00:00 2001 From: paul-dingemans Date: Sun, 14 May 2023 13:31:33 +0200 Subject: [PATCH 80/80] Refactor: * extract filename to method * wrap filename between single quotes instead of backticks * replace check of empty file * also ignore files that only contain comments --- .../ruleset/standard/rules/NoEmptyFileRule.kt | 36 +++++---- .../standard/rules/NoEmptyFileRuleTest.kt | 81 ++++++++++++++----- 2 files changed, 82 insertions(+), 35 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt index a705930f9e..49bb8921a3 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRule.kt @@ -3,6 +3,7 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.ElementType import com.pinterest.ktlint.rule.engine.core.api.Rule import com.pinterest.ktlint.rule.engine.core.api.children +import com.pinterest.ktlint.rule.engine.core.api.isPartOfComment import com.pinterest.ktlint.rule.engine.core.api.isRoot import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace import com.pinterest.ktlint.ruleset.standard.StandardRule @@ -17,24 +18,25 @@ public class NoEmptyFileRule : StandardRule(id = "no-empty-file"), Rule.Experime node .takeIf { it.isRoot() } ?.takeIf { it.isEmptyFile() } - ?.let { - val fileName = - node.psi.containingFile.virtualFile.name - .replace("\\", "/") // Ensure compatibility with Windows OS - .substringAfterLast("/") - emit(0, "File `$fileName` should not be empty", false) - } + ?.let { emit(0, "File '${node.getFileName()}' should not be empty", false) } } - private fun ASTNode.isEmptyFile(): Boolean { - if (text.isBlank()) return true + private fun ASTNode.getFileName() = + psi + .containingFile + .virtualFile + .name + .replace("\\", "/") // Ensure compatibility with Windows OS + .substringAfterLast("/") - return this.children() - .toList() - .filter { - !it.isWhiteSpace() && - it.elementType != ElementType.PACKAGE_DIRECTIVE && - it.elementType != ElementType.IMPORT_LIST - }.isEmpty() - } + private fun ASTNode.isEmptyFile(): Boolean = + null == + children() + .firstOrNull { + !it.isWhiteSpace() && + !it.isPartOfComment() && + it.elementType != ElementType.PACKAGE_DIRECTIVE && + it.elementType != ElementType.IMPORT_LIST && + !(it.elementType == ElementType.SCRIPT && it.text.isBlank()) + } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt index 1aa2bc6cc3..e2caad3f74 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoEmptyFileRuleTest.kt @@ -10,14 +10,12 @@ class NoEmptyFileRuleTest { fun `Given non-empty kotlin file then ignore the rule for this file`() { val code = """ - package tmp - fun main(){ - println("Hello world") + package foo + fun main() { + println("foo") } """.trimIndent() - noEmptyFileRuleAssertThat(code) - .asFileWithPath("/some/path/Tmp.kt") - .hasNoLintViolations() + noEmptyFileRuleAssertThat(code).hasNoLintViolations() } @Test @@ -25,7 +23,7 @@ class NoEmptyFileRuleTest { val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .hasLintViolationWithoutAutoCorrect(1, 1, "File `Tmp.kt` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File 'Tmp.kt' should not be empty") } @Test @@ -33,51 +31,74 @@ class NoEmptyFileRuleTest { val code = EMPTY_FILE noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kts") - .hasLintViolationWithoutAutoCorrect(1, 1, "File `Tmp.kts` should not be empty") + .asKotlinScript() + .hasLintViolationWithoutAutoCorrect(1, 1, "File 'Tmp.kts' should not be empty") } @Test fun `Given only package statement in kotlin file then do a return lint error`() { val code = """ - package path + package foo """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .hasLintViolationWithoutAutoCorrect(1, 1, "File `Tmp.kt` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File 'Tmp.kt' should not be empty") } @Test fun `Given only import statement in kotlin file then do a return lint error`() { val code = """ - import sample.Hello + import foo.Bar """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .hasLintViolationWithoutAutoCorrect(1, 1, "File `Tmp.kt` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File 'Tmp.kt' should not be empty") } @Test fun `Given only package and import statements in kotlin file then do a return lint error`() { val code = """ - package path - import sample.Hello + package foo + import foo.Bar + """.trimIndent() + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kt") + .hasLintViolationWithoutAutoCorrect(1, 1, "File 'Tmp.kt' should not be empty") + } + + @Test + fun `Given only package, import statements and comments in kotlin file then do a return lint error`() { + val code = + """ + package foo + import foo.Bar + + // some comment + + /* + * some comment + */ + + /** + * some comment + */ """.trimIndent() noEmptyFileRuleAssertThat(code) .asFileWithPath("/some/path/Tmp.kt") - .hasLintViolationWithoutAutoCorrect(1, 1, "File `Tmp.kt` should not be empty") + .hasLintViolationWithoutAutoCorrect(1, 1, "File 'Tmp.kt' should not be empty") } @Test fun `Given non-empty kotlin file then ignore this file`() { val code = """ - package tmp - fun main(){ - println("Hello world") + package foo + fun main() { + println("foo") } """.trimIndent() noEmptyFileRuleAssertThat(code) @@ -85,6 +106,30 @@ class NoEmptyFileRuleTest { .hasNoLintViolations() } + @Test + fun `x x`() { + val code = + """ + plugins { + id("ktlint-publication-library") + } + + dependencies { + implementation(projects.ktlintLogger) + implementation(projects.ktlintRuleEngine) + implementation(projects.ktlintCliRulesetCore) + api(libs.assertj) + api(libs.junit5) + api(libs.janino) + api(libs.jimfs) + } + """.trimIndent() + noEmptyFileRuleAssertThat(code) + .asFileWithPath("/some/path/Tmp.kts") + .asKotlinScript() + .hasNoLintViolations() + } + private companion object { private const val EMPTY_FILE = "" }