diff --git a/CHANGELOG.md b/CHANGELOG.md index e4213870c3..53b971c00e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Added -* Add rule `class-signature`. This rule rewrites the class header to a consistent format. In code style `ktlint_official`, super types are always wrapped to a separate line. In other code styles, super types are only wrapped in classes having multiple super types. Especially for code style `ktlint_official` the class headers are rewritten in a more consistent format. See [examples in documentation](https://pinterest.github.io/ktlint/latest/rules/experimental/#class-signature). `class-signature` [#875](https://github.com/pinterest/ktlint/issues/1349), [#1349](https://github.com/pinterest/ktlint/issues/875) +* Add experimental rule `class-signature`. This rule rewrites the class header to a consistent format. In code style `ktlint_official`, super types are always wrapped to a separate line. In other code styles, super types are only wrapped in classes having multiple super types. Especially for code style `ktlint_official` the class headers are rewritten in a more consistent format. See [examples in documentation](https://pinterest.github.io/ktlint/latest/rules/experimental/#class-signature). `class-signature` [#875](https://github.com/pinterest/ktlint/issues/1349), [#1349](https://github.com/pinterest/ktlint/issues/875) +* Add experimental rule `function-expression-body`. This rule rewrites function bodies only contain a `return` or `throw` expression to an expression body. [#2150](https://github.com/pinterest/ktlint/issues/2150) ### Removed @@ -34,7 +35,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). * Update dependency com.google.jimfs:jimfs to v1.3.0 ([#2112](https://github.com/pinterest/ktlint/pull/2112)) * As a part of public API stabilization, configure `binary-compatibility-validator` plugin for compile-time verification of binary compatibility with previous `ktlint` versions ([#2131](https://github.com/pinterest/ktlint/pull/2131)) * Update dependency org.junit.jupiter:junit-jupiter to v5.10.0 ([#2148](https://github.com/pinterest/ktlint/pull/2148)) -* + ## [0.50.0] - 2023-06-29 ### Deprecation of ktlint-enable and ktlint-disable directives diff --git a/documentation/snapshot/docs/rules/experimental.md b/documentation/snapshot/docs/rules/experimental.md index 0c64d7cc75..d5d0bf7083 100644 --- a/documentation/snapshot/docs/rules/experimental.md +++ b/documentation/snapshot/docs/rules/experimental.md @@ -434,6 +434,55 @@ This rule is only run when `ktlint_code_style` is set to `ktlint_official` or wh ## Function signature +Rewrites a function body only containing a `return` or `throw` expression to an expression body. + +!!! note: + If the function body contains a comment, it is not rewritten to an expression body. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + fun foo1() = "foo" + fun foo2(): String = "foo" + fun foo3(): Unit = throw IllegalArgumentException("some message") + fun foo4(): Foo = throw IllegalArgumentException("some message") + fun foo5() { + return "foo" // some comment + } + fun foo6(): String { + /* some comment */ + return "foo" + } + fun foo7() { + throw IllegalArgumentException("some message") + /* some comment */ + } + fun foo8(): Foo { + throw IllegalArgumentException("some message") + // some comment + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + fun foo1() { + return "foo" + } + fun foo2(): String { + return "foo" + } + fun foo3() { + throw IllegalArgumentException("some message") + } + fun foo4(): Foo { + throw IllegalArgumentException("some message") + } + ``` + +Rule id: `function-expression-body` (`standard` rule set) + +## 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" diff --git a/ktlint-cli-reporter-format/src/main/kotlin/com/pinterest/ktlint/cli/reporter/format/FormatReporterProvider.kt b/ktlint-cli-reporter-format/src/main/kotlin/com/pinterest/ktlint/cli/reporter/format/FormatReporterProvider.kt index 0443b349b3..5239640661 100644 --- a/ktlint-cli-reporter-format/src/main/kotlin/com/pinterest/ktlint/cli/reporter/format/FormatReporterProvider.kt +++ b/ktlint-cli-reporter-format/src/main/kotlin/com/pinterest/ktlint/cli/reporter/format/FormatReporterProvider.kt @@ -22,7 +22,8 @@ public class FormatReporterProvider : ReporterProviderV2 { private fun String.emptyOrTrue() = this == "" || this == "true" - private fun getColor(color: String?): Color { - return Color.values().firstOrNull { it.name == color } ?: throw IllegalArgumentException("Invalid color parameter.") - } + private fun getColor(color: String?): Color = + Color.values().firstOrNull { + it.name == color + } ?: throw IllegalArgumentException("Invalid color parameter.") } diff --git a/ktlint-cli-reporter-plain/src/main/kotlin/com/pinterest/ktlint/cli/reporter/plain/PlainReporterProvider.kt b/ktlint-cli-reporter-plain/src/main/kotlin/com/pinterest/ktlint/cli/reporter/plain/PlainReporterProvider.kt index b5ae3c82db..f3802c7282 100644 --- a/ktlint-cli-reporter-plain/src/main/kotlin/com/pinterest/ktlint/cli/reporter/plain/PlainReporterProvider.kt +++ b/ktlint-cli-reporter-plain/src/main/kotlin/com/pinterest/ktlint/cli/reporter/plain/PlainReporterProvider.kt @@ -20,7 +20,8 @@ public class PlainReporterProvider : ReporterProviderV2 { private fun String.emptyOrTrue() = this == "" || this == "true" - private fun getColor(colorInput: String?): Color { - return Color.values().firstOrNull { it.name == colorInput } ?: throw IllegalArgumentException("Invalid color parameter.") - } + private fun getColor(colorInput: String?): Color = + Color.values().firstOrNull { + it.name == colorInput + } ?: throw IllegalArgumentException("Invalid color parameter.") } 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 62506a50fb..da07757c8a 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 @@ -134,15 +134,14 @@ internal fun FileSystem.fileSequence( override fun preVisitDirectory( dirPath: Path, dirAttr: BasicFileAttributes, - ): FileVisitResult { - return if (Files.isHidden(dirPath)) { + ): FileVisitResult = + if (Files.isHidden(dirPath)) { LOGGER.trace { "- Dir: $dirPath: Ignore" } FileVisitResult.SKIP_SUBTREE } else { LOGGER.trace { "- Dir: $dirPath: Traverse" } FileVisitResult.CONTINUE } - } }, ) } 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 9961d6deaf..55f64130e5 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 @@ -654,28 +654,18 @@ internal class KtlintCommandLine { ) { val pill = object : Future { - override fun isDone(): Boolean { - throw UnsupportedOperationException() - } + override fun isDone(): Boolean = throw UnsupportedOperationException() override fun get( timeout: Long, unit: TimeUnit, - ): T { - throw UnsupportedOperationException() - } + ): T = throw UnsupportedOperationException() - override fun get(): T { - throw UnsupportedOperationException() - } + override fun get(): T = throw UnsupportedOperationException() - override fun cancel(mayInterruptIfRunning: Boolean): Boolean { - throw UnsupportedOperationException() - } + override fun cancel(mayInterruptIfRunning: Boolean): Boolean = throw UnsupportedOperationException() - override fun isCancelled(): Boolean { - throw UnsupportedOperationException() - } + override fun isCancelled(): Boolean = throw UnsupportedOperationException() } val q = ArrayBlockingQueue>(numberOfThreads) val producer = diff --git a/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/RuleProvider.kt b/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/RuleProvider.kt index 48d0dd089c..04781566f6 100644 --- a/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/RuleProvider.kt +++ b/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/RuleProvider.kt @@ -28,9 +28,7 @@ public class RuleProvider private constructor( /** * Creates a new [Rule] instance. */ - public fun createNewRuleInstance(): Rule { - return provider() - } + public fun createNewRuleInstance(): Rule = provider() /** * Lambda which creates a new instance of the [Rule]. Important: to ensure that a [Rule] can keep internal state and that processing of diff --git a/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfig.kt b/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfig.kt index 86e9850800..5fadceda3d 100644 --- a/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfig.kt +++ b/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfig.kt @@ -198,13 +198,12 @@ public data class EditorConfig( } } - private fun Set>.defaultProperties(): Map { - return associate { editorConfigProperty -> + private fun Set>.defaultProperties(): Map = + associate { editorConfigProperty -> editorConfigProperty .writeDefaultValue() .let { editorConfigProperty.name to editorConfigProperty.toPropertyWithValue(it) } } - } private fun EditorConfigProperty.writeDefaultValue() = propertyWriter(getDefaultValue()) diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleExecutionContext.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleExecutionContext.kt index a6554d4365..9c4c0303f6 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleExecutionContext.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleExecutionContext.kt @@ -204,12 +204,11 @@ internal class RuleExecutionContext private constructor( ) } - private fun normalizeText(text: String): String { - return text + private fun normalizeText(text: String): String = + text .replace("\r\n", "\n") .replace("\r", "\n") .replaceFirst(UTF8_BOM, "") - } private fun PsiElement.findErrorElement(): PsiErrorElement? { if (this is PsiErrorElement) { diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RunAfterRuleFilter.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RunAfterRuleFilter.kt index 1c94817544..3a8d5aaac3 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RunAfterRuleFilter.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RunAfterRuleFilter.kt @@ -150,8 +150,8 @@ internal class RunAfterRuleFilter : RuleFilter { private fun Set.canRunWith(loadedRuleProviders: Set): Set = canRunWithRuleIds(loadedRuleProviders.map { it.ruleId }.toSet()) - private fun Set.canRunWithRuleIds(loadedRuleIds: Set): Set { - return this + private fun Set.canRunWithRuleIds(loadedRuleIds: Set): Set = + this .filter { it.canRunWith(loadedRuleIds) } .let { unblockedRuleProviders -> if (unblockedRuleProviders.isEmpty()) { @@ -165,7 +165,6 @@ internal class RunAfterRuleFilter : RuleFilter { .plus(unblockedRuleProviders) } }.toSet() - } private fun RuleProvider.canRunWith(loadedRuleIds: Set): Boolean = this diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/KtlintSuppressionRule.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/KtlintSuppressionRule.kt index eee3e33b96..6f569c1f04 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/KtlintSuppressionRule.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rules/KtlintSuppressionRule.kt @@ -456,8 +456,8 @@ public class KtlintSuppressionRule( private fun ASTNode.createFileAnnotation( suppressType: SuppressAnnotationType, suppressions: Set, - ): ASTNode { - return suppressions + ): ASTNode = + suppressions .sorted() .joinToString() .let { sortedSuppressions -> "@file:${suppressType.annotationName}($sortedSuppressions)" } @@ -468,7 +468,6 @@ public class KtlintSuppressionRule( ?.firstChild ?: throw IllegalStateException("Can not create annotation '$annotation'") }.node - } private fun ASTNode.createFileAnnotationList(annotation: ASTNode) { require(isRoot()) { "File annotation list can only be created for root node" } diff --git a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleProviderSorterTest.kt b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleProviderSorterTest.kt index ff3501a076..de461b7dc3 100644 --- a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleProviderSorterTest.kt +++ b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleProviderSorterTest.kt @@ -468,10 +468,9 @@ class RuleProviderSorterTest { node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - ) { + ): Unit = throw UnsupportedOperationException( "Rule should never be really invoked because that is not the aim of this unit test.", ) - } } } diff --git a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/InternalRuleProvidersFilterTest.kt b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/InternalRuleProvidersFilterTest.kt index 686b463830..2a6ce8388c 100644 --- a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/InternalRuleProvidersFilterTest.kt +++ b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/InternalRuleProvidersFilterTest.kt @@ -51,11 +51,10 @@ class InternalRuleProvidersFilterTest { node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - ) { + ): Unit = throw UnsupportedOperationException( "Rule should never be really invoked because that is not the aim of this unit test.", ) - } } private fun Set.toRuleId() = map { it.ruleId } diff --git a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RunAfterRuleFilterTest.kt b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RunAfterRuleFilterTest.kt index 6e0ca177ec..e71e5041de 100644 --- a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RunAfterRuleFilterTest.kt +++ b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RunAfterRuleFilterTest.kt @@ -373,11 +373,10 @@ class RunAfterRuleFilterTest { node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - ) { + ): Unit = throw UnsupportedOperationException( "Rule should never be really invoked because that is not the aim of this unit test.", ) - } } private fun createRuleProviders(vararg rules: Rule) = diff --git a/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api b/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api index c0cb83061b..bc1d17295d 100644 --- a/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api +++ b/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api @@ -184,6 +184,16 @@ public final class com/pinterest/ktlint/ruleset/standard/rules/FunKeywordSpacing public static final fun getFUN_KEYWORD_SPACING_RULE ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId; } +public final class com/pinterest/ktlint/ruleset/standard/rules/FunctionExpressionBodyKt { + public static final fun getFUNCTION_EXPRESSION_BODY_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId; +} + +public final class com/pinterest/ktlint/ruleset/standard/rules/FunctionExpressionBodyRule : com/pinterest/ktlint/ruleset/standard/StandardRule, com/pinterest/ktlint/rule/engine/core/api/Rule$Experimental { + public fun ()V + public fun beforeFirstNode (Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfig;)V + public fun beforeVisitChildNodes (Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;ZLkotlin/jvm/functions/Function3;)V +} + public final class com/pinterest/ktlint/ruleset/standard/rules/FunctionLiteralKt { public static final fun getFUNCTION_LITERAL_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId; } 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 6fc7a42b37..7480941fb9 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 @@ -21,6 +21,7 @@ import com.pinterest.ktlint.ruleset.standard.rules.EnumWrappingRule import com.pinterest.ktlint.ruleset.standard.rules.FilenameRule import com.pinterest.ktlint.ruleset.standard.rules.FinalNewlineRule import com.pinterest.ktlint.ruleset.standard.rules.FunKeywordSpacingRule +import com.pinterest.ktlint.ruleset.standard.rules.FunctionExpressionBodyRule import com.pinterest.ktlint.ruleset.standard.rules.FunctionLiteralRule import com.pinterest.ktlint.ruleset.standard.rules.FunctionNamingRule import com.pinterest.ktlint.ruleset.standard.rules.FunctionReturnTypeSpacingRule @@ -106,6 +107,7 @@ public class StandardRuleSetProvider : RuleSetProviderV3(RuleSetId.STANDARD) { RuleProvider { EnumWrappingRule() }, RuleProvider { FilenameRule() }, RuleProvider { FinalNewlineRule() }, + RuleProvider { FunctionExpressionBodyRule() }, RuleProvider { FunctionLiteralRule() }, RuleProvider { FunctionNamingRule() }, RuleProvider { FunctionReturnTypeSpacingRule() }, diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRule.kt index 8c316b689e..ae51cf439b 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRule.kt @@ -228,14 +228,13 @@ public class ArgumentListWrappingRule : else -> throw UnsupportedOperationException() } - private fun ASTNode.textContainsIgnoringLambda(char: Char): Boolean { - return children().any { child -> + private fun ASTNode.textContainsIgnoringLambda(char: Char): Boolean = + children().any { child -> val elementType = child.elementType elementType == ElementType.WHITE_SPACE && child.textContains(char) || elementType == ElementType.COLLECTION_LITERAL_EXPRESSION && child.textContains(char) || elementType == ElementType.VALUE_ARGUMENT && child.children().any { it.textContainsIgnoringLambda(char) } } - } private fun ASTNode.hasTypeArgumentListInFront(): Boolean = treeParent.children() diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ChainWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ChainWrappingRule.kt index 763dca5803..28fcca3cc9 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ChainWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ChainWrappingRule.kt @@ -143,10 +143,9 @@ public class ChainWrappingRule : private fun ASTNode.isInPrefixPosition() = treeParent?.treeParent?.elementType == PREFIX_EXPRESSION - private fun ASTNode.isElvisOperatorAndComment(): Boolean { - return elementType == ELVIS && + private fun ASTNode.isElvisOperatorAndComment(): Boolean = + elementType == ELVIS && leaves().takeWhile { it.isWhiteSpaceWithoutNewline() || it.isPartOfComment() }.any() - } } public val CHAIN_WRAPPING_RULE_ID: RuleId = ChainWrappingRule().ruleId diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionExpressionBody.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionExpressionBody.kt new file mode 100644 index 0000000000..bbb46468b5 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionExpressionBody.kt @@ -0,0 +1,172 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.rule.engine.core.api.ElementType.BLOCK +import com.pinterest.ktlint.rule.engine.core.api.ElementType.COLON +import com.pinterest.ktlint.rule.engine.core.api.ElementType.EQ +import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUN +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.ElementType.RETURN +import com.pinterest.ktlint.rule.engine.core.api.ElementType.RETURN_KEYWORD +import com.pinterest.ktlint.rule.engine.core.api.ElementType.THROW +import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_REFERENCE +import com.pinterest.ktlint.rule.engine.core.api.IndentConfig +import com.pinterest.ktlint.rule.engine.core.api.Rule +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.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.isWhiteSpace +import com.pinterest.ktlint.rule.engine.core.api.nextSibling +import com.pinterest.ktlint.rule.engine.core.api.prevSibling +import com.pinterest.ktlint.ruleset.standard.StandardRule +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl +import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType +import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtFunction +import org.jetbrains.kotlin.psi.KtScript +import org.jetbrains.kotlin.psi.KtTypeReference +import org.jetbrains.kotlin.psi.psiUtil.getChildOfType + +/** + * [Kotlin Coding Conventions](https://kotlinlang.org/docs/coding-conventions.html#functions): + * Prefer using an expression body for functions with the body consisting of a single expression. + * ``` + * override fun toString(): String { + * return "Hey" + * } + * > + * override fun toString(): String = "Hey" + * ``` + * + * [Android Kotlin styleguide](https://developer.android.com/kotlin/style-guide#expression_functions): + * + * When a function contains only a single expression it can be represented as an [expression function] + * ``` + * override fun toString(): String { + * return "Hey" + * } + * > + * override fun toString(): String { + * return "Hey" + * } + * ``` + */ +public class FunctionExpressionBodyRule : + StandardRule( + id = "function-expression-body", + usesEditorConfigProperties = + setOf( + CODE_STYLE_PROPERTY, + INDENT_SIZE_PROPERTY, + INDENT_STYLE_PROPERTY, + MAX_LINE_LENGTH_PROPERTY, + ), + ), + Rule.Experimental { + private var codeStyle = CODE_STYLE_PROPERTY.defaultValue + private var indentConfig = IndentConfig.DEFAULT_INDENT_CONFIG + private var maxLineLength = MAX_LINE_LENGTH_PROPERTY.defaultValue + + override fun beforeFirstNode(editorConfig: EditorConfig) { + codeStyle = editorConfig[CODE_STYLE_PROPERTY] + maxLineLength = editorConfig[MAX_LINE_LENGTH_PROPERTY] + indentConfig = + IndentConfig( + indentStyle = editorConfig[INDENT_STYLE_PROPERTY], + tabWidth = editorConfig[INDENT_SIZE_PROPERTY], + ) + if (indentConfig.disabled) { + stopTraversalOfAST() + } + } + + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + node + .takeIf { it.elementType == BLOCK && it.treeParent.elementType == FUN } + ?.let { visitFunctionBody(node, autoCorrect, emit) } + } + + private fun visitFunctionBody( + block: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + require(block.elementType == BLOCK) + block + .takeIf { it.containingOnly(RETURN) } + ?.findChildByType(RETURN) + ?.findChildByType(RETURN_KEYWORD) + ?.nextSibling { !it.isWhiteSpace() } + ?.let { codeSibling -> + emit(block.startOffset, "Function body should be replaced with body expression", true) + if (autoCorrect) { + with(block.treeParent) { + // Insert the code sibling before the block + addChild(LeafPsiElement(EQ, "="), block) + addChild(PsiWhiteSpaceImpl(" "), block) + addChild(codeSibling, block) + // Remove old (and now empty block) + removeChild(block) + } + } + } + block + .takeIf { it.containingOnly(THROW) } + ?.findChildByType(THROW) + ?.let { throwNode -> + emit(block.startOffset, "Function body should be replaced with body expression", true) + if (autoCorrect) { + with(block.treeParent) { + // Remove whitespace before block + block + .prevSibling() + .takeIf { it.isWhiteSpace() } + ?.let { removeChild(it) } + if (findChildByType(TYPE_REFERENCE) == null) { + // Insert Unit as return type as otherwise a compilation error results + addChild(LeafPsiElement(COLON, ":"), block) + addChild(PsiWhiteSpaceImpl(" "), block) + addChild(createUnitTypeReference(), block) + } + addChild(PsiWhiteSpaceImpl(" "), block) + addChild(LeafPsiElement(EQ, "="), block) + addChild(PsiWhiteSpaceImpl(" "), block) + addChild(throwNode, block) + // Remove old (and now empty block) + removeChild(block) + } + } + } + } + + private fun ASTNode.containingOnly(iElementType: IElementType) = + iElementType == + children() + .filterNot { it.elementType == LBRACE || it.elementType == RBRACE || it.isWhiteSpace() } + .singleOrNull() + ?.elementType + + private fun ASTNode.createUnitTypeReference() = + PsiFileFactory + .getInstance(psi.project) + .createFileFromText(KotlinLanguage.INSTANCE, "fun foo(): Unit {}") + .getChildOfType() + ?.getChildOfType() + ?.getChildOfType() + ?.getChildOfType() + ?.node!! +} + +public val FUNCTION_EXPRESSION_BODY_RULE_ID: RuleId = FunctionExpressionBodyRule().ruleId diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseWrappingRule.kt index ed08a29167..13c02ccd3e 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseWrappingRule.kt @@ -164,25 +164,14 @@ public class IfElseWrappingRule : } } -// private fun ASTNode.findFirstNodeInBlockToBeIndented(): ASTNode? { -// val blockOrSelf = findChildByType(BLOCK) ?: this -// return blockOrSelf -// .children() -// .find { -// it.elementType != LBRACE && -// !it.isWhitespaceBeforeComment() && -// !it.isPartOfComment() -// } -// } - private fun ASTNode.findFirstNodeInBlockToBeIndented(): ASTNode? { - return findChildByType(BLOCK) + private fun ASTNode.findFirstNodeInBlockToBeIndented(): ASTNode? = + findChildByType(BLOCK) ?.children() ?.first { it.elementType != LBRACE && !it.isWhitespaceBeforeComment() && !it.isPartOfComment() } - } private fun ASTNode.isWhitespaceBeforeComment() = isWhiteSpaceWithoutNewline() && nextLeaf()?.isPartOfComment() == true diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ImportOrderingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ImportOrderingRule.kt index 3876514190..93e413d47a 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ImportOrderingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ImportOrderingRule.kt @@ -173,9 +173,11 @@ public class ImportOrderingRule : private fun isCustomLayout() = importsLayout != IDEA_PATTERN && importsLayout != ASCII_PATTERN - private fun hasTooMuchWhitespace(nodes: Array): Boolean { - return nodes.any { it is PsiWhiteSpace && (it as PsiWhiteSpace).text != "\n" } - } + private fun hasTooMuchWhitespace(nodes: Array): Boolean = + nodes.any { + it is PsiWhiteSpace && + (it as PsiWhiteSpace).text != "\n" + } public companion object { /** diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MaxLineLengthRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MaxLineLengthRule.kt index 57d42a50b0..4ec0b52dad 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MaxLineLengthRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MaxLineLengthRule.kt @@ -146,20 +146,18 @@ private data class ParsedLine( val offset: Int, val elements: List, ) { - fun lineLength(ignoreBackTickedIdentifier: Boolean): Int { - return if (ignoreBackTickedIdentifier) { + fun lineLength(ignoreBackTickedIdentifier: Boolean): Int = + if (ignoreBackTickedIdentifier) { line.length - totalLengthBacktickedElements() } else { line.length } - } - private fun totalLengthBacktickedElements(): Int { - return elements + private fun totalLengthBacktickedElements(): Int = + elements .filterIsInstance(PsiElement::class.java) .filter { it.text.matches(BACKTICKED_IDENTIFIER_REGEX) } .sumOf(PsiElement::getTextLength) - } private companion object { val BACKTICKED_IDENTIFIER_REGEX = Regex("`.*`") diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListSpacingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListSpacingRule.kt index ca8a7c63a1..c92583110a 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListSpacingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListSpacingRule.kt @@ -253,8 +253,8 @@ public class ParameterListSpacingRule : } } - private fun ASTNode.getPrecedingModifier(): ASTNode? { - return prevCodeSibling() + private fun ASTNode.getPrecedingModifier(): ASTNode? = + prevCodeSibling() ?.let { prevCodeSibling -> if (prevCodeSibling.elementType == MODIFIER_LIST) { prevCodeSibling.lastChildNode @@ -263,7 +263,6 @@ public class ParameterListSpacingRule : prevCodeSibling } } - } private fun ASTNode?.isTypeReferenceWithModifierList() = null != diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/internal/importordering/PatternEntry.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/internal/importordering/PatternEntry.kt index 929c15ef73..c1cce41b74 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/internal/importordering/PatternEntry.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/internal/importordering/PatternEntry.kt @@ -29,9 +29,7 @@ public class PatternEntry( return false } - internal fun matches(import: ImportPath): Boolean { - return matchesPackageName(import.pathStr.removeSuffix(".*")) - } + internal fun matches(import: ImportPath): Boolean = matchesPackageName(import.pathStr.removeSuffix(".*")) internal fun isBetterMatchForPackageThan( entry: PatternEntry?, diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionExpressionBodyRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionExpressionBodyRuleTest.kt new file mode 100644 index 0000000000..7c2fa3b07e --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionExpressionBodyRuleTest.kt @@ -0,0 +1,156 @@ +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.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class FunctionExpressionBodyRuleTest { + private val functionExpressionBodyRule = assertThatRule { FunctionExpressionBodyRule() } + + @Test + fun `Given a function body without any statement`() { + val code = + """ + fun foo() { + } + """.trimIndent() + functionExpressionBodyRule(code).hasNoLintViolations() + } + + @Test + fun `Given a function body with a comment but without any statement`() { + val code = + """ + fun foo() { + // some comment + } + """.trimIndent() + functionExpressionBodyRule(code).hasNoLintViolations() + } + + @Test + fun `Given a function body with a return statement`() { + val code = + """ + fun foo() { + return "foo" + } + """.trimIndent() + val formattedCode = + """ + fun foo() = "foo" + """.trimIndent() + functionExpressionBodyRule(code) + .isFormattedAs(formattedCode) + .hasLintViolation(1, 11, "Function body should be replaced with body expression") + } + + @Test + fun `Given a function with a return type and a body with a return statement`() { + val code = + """ + fun foo(): String { + return "foo" + } + """.trimIndent() + val formattedCode = + """ + fun foo(): String = "foo" + """.trimIndent() + functionExpressionBodyRule(code) + .isFormattedAs(formattedCode) + .hasLintViolation(1, 19, "Function body should be replaced with body expression") + } + + @Test + fun `Given a function body with a multiline expression as return statement`() { + val code = + """ + fun foo(bar: Boolean) { + return if (bar) { + "bar" + } else { + "foo" + } + } + """.trimIndent() + val formattedCode = + """ + fun foo(bar: Boolean) = if (bar) { + "bar" + } else { + "foo" + } + """.trimIndent() + functionExpressionBodyRule(code) + .isFormattedAs(formattedCode) + .hasLintViolation(1, 23, "Function body should be replaced with body expression") + } + + @ParameterizedTest(name = "Body: {0}") + @ValueSource( + strings = [ + """ + fun foo() { + // some comment + return "foo" + } + """, + """ + fun foo() { + return "foo" // some comment + } + """, + """ + fun foo() { + return "foo" + // some comment + } + """, + """ + fun foo() { + val bar = bar() + return "foo" + } + """, + ], + ) + fun `Given a function body with a return statement and a comment or other code leaf`(code: String) { + functionExpressionBodyRule(code.trimIndent()).hasNoLintViolations() + } + + @Test + fun `Given a function body with throws expression as only statement then convert to body expression and add Unit return type`() { + val code = + """ + fun foo() { + throw IllegalArgumentException("some message") + } + """.trimIndent() + val formattedCode = + """ + fun foo(): Unit = throw IllegalArgumentException("some message") + """.trimIndent() + functionExpressionBodyRule(code) + .isFormattedAs(formattedCode) + .hasLintViolation(1, 11, "Function body should be replaced with body expression") + } + + @Test + fun `Given a function with return type and body with throws expression as only statement then convert to body expression and keep original return type`() { + val code = + """ + fun foo(): Foo { + throw IllegalArgumentException("some message") + } + """.trimIndent() + val formattedCode = + """ + fun foo(): Foo = throw IllegalArgumentException("some message") + """.trimIndent() + functionExpressionBodyRule(code) + .isFormattedAs(formattedCode) + .hasLintViolation(1, 16, "Function body should be replaced with body expression") + } +} diff --git a/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/KtLintAssertThat.kt b/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/KtLintAssertThat.kt index e2e8ca90ef..a3590e8522 100644 --- a/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/KtLintAssertThat.kt +++ b/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/KtLintAssertThat.kt @@ -572,8 +572,8 @@ public class KtLintAssertThatAssertable( .containsExactlyInAnyOrder(*expectedLintViolationFields.toTypedArray()) } - private fun Array.toLintViolationsFields(): Array { - return map { + private fun Array.toLintViolationsFields(): Array = + map { LintViolationFields( line = it.line, col = it.col, @@ -582,7 +582,6 @@ public class KtLintAssertThatAssertable( ) }.distinct() .toTypedArray() - } private fun Set.filterAdditionalRulesOnly() = filter { it.ruleId != ruleId }.toSet()