Skip to content

Commit 47c536f

Browse files
Add context-receiver-list-wrapping rule (#3034)
* Ignore context parameters in rule `context-receiver-wrapping` Closes #3028 * Add `context-receiver-list-wrapping` rule Similar to `context-receiver-list` but it only wraps when the list contains a context parameter instead of a context receiver Closes #3029 * Fix API contract
1 parent 078e439 commit 47c536f

5 files changed

Lines changed: 521 additions & 1 deletion

File tree

documentation/snapshot/docs/rules/standard.md

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4367,7 +4367,13 @@ Suppress or disable rule (1)
43674367

43684368
### Context receiver wrapping
43694369

4370-
Wraps the context receiver list of a function 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.
4370+
!!! important
4371+
Context receivers are deprecated starting from Kotlin 2.2.0 and will be removed in a future version. Once KtLint is upgraded to a Kotlin version that no longer supports context receivers, then you will not be able to use that KtLint version as long as your code still contains context receiver.
4372+
4373+
!!! tip
4374+
This rule does not affect context parameters. See rule `context-receiver-list-wrapping` for wrapping of context parameters.
4375+
4376+
Wraps the context receiver list containing a context receiver 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.
43714377

43724378
=== "[:material-heart:](#) Ktlint"
43734379

@@ -4438,6 +4444,83 @@ Suppress or disable rule (1)
44384444
```editorconfig
44394445
ktlint_standard_context-receiver-wrapping = disabled
44404446
```
4447+
4448+
### Context receiver list wrapping
4449+
4450+
!!! tip
4451+
This rule does not affect context receivers. See rule `context-receiver-wrapping` for wrapping of context receivers.
4452+
4453+
Wraps the context receiver list containing a context parameter 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.
4454+
4455+
=== "[:material-heart:](#) Ktlint"
4456+
4457+
```kotlin
4458+
// Always wrap regardless of whether max line length is set
4459+
context(_: Foo)
4460+
fun fooBar()
4461+
4462+
// Wrap each context receiver to a separate line when the
4463+
// entire context receiver list does not fit on a single line
4464+
context(
4465+
foo1: Fooooooooooooooooooo1,
4466+
foo2: Foooooooooooooooooooooooooooooo2
4467+
)
4468+
fun fooBar()
4469+
4470+
// Wrap each context receiver to a separate line when the
4471+
// entire context receiver list does not fit on a single line.
4472+
// Also, wrap each of it projection types in case a context
4473+
// receiver does not fit on a single line after it has been
4474+
// wrapped.
4475+
context(
4476+
_: Foooooooooooooooo<
4477+
Foo,
4478+
Bar,
4479+
>
4480+
)
4481+
fun fooBar()
4482+
```
4483+
4484+
=== "[:material-heart-off-outline:](#) Disallowed"
4485+
4486+
```kotlin
4487+
// Should be wrapped regardless of whether max line length is set
4488+
context(_: Foo) fun fooBar()
4489+
4490+
// Should be wrapped when the entire context receiver list does not
4491+
// fit on a single line
4492+
context(foo1: Fooooooooooooooooooo1, foo2: Foooooooooooooooooooooooooooooo2)
4493+
fun fooBar()
4494+
4495+
// Should be wrapped when the entire context receiver list does not
4496+
// fit on a single line. Also, it should wrap each of it projection
4497+
// type in case a context receiver does not fit on a single line
4498+
// after it has been wrapped.
4499+
context(_: Foooooooooooooooo<Foo, Bar>)
4500+
fun fooBar()
4501+
```
4502+
4503+
| Configuration setting | ktlint_official | intellij_idea | android_studio |
4504+
|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------:|:-------------:|:--------------:|
4505+
| `max_line_length`<br/><i>Maximum length of a (regular) line. This property is ignored in case the `max-line-length` rule is disabled, or when using Ktlint via a third party integration that does not provide this rule.</i> | 140 | `off` | `100` |
4506+
4507+
Rule id: `standard:context-receiver-list-wrapping`
4508+
4509+
Suppress or disable rule (1)
4510+
{ .annotate }
4511+
4512+
1. Suppress rule in code with annotation below:
4513+
```kotlin
4514+
@Suppress("ktlint:standard:context-receiver-list-wrapping")
4515+
```
4516+
Enable rule via `.editorconfig`
4517+
```editorconfig
4518+
ktlint_standard_context-receiver-list-wrapping = enabled
4519+
```
4520+
Disable rule via `.editorconfig`
4521+
```editorconfig
4522+
ktlint_standard_context-receiver-list-wrapping = disabled
4523+
```
44414524

44424525
### Enum wrapping
44434526

ktlint-ruleset-standard/api/ktlint-ruleset-standard.api

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,16 @@ public final class com/pinterest/ktlint/ruleset/standard/rules/ConditionWrapping
180180
public static final fun getCONDITION_WRAPPING_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId;
181181
}
182182

183+
public final class com/pinterest/ktlint/ruleset/standard/rules/ContextReceiverListWrappingRule : com/pinterest/ktlint/ruleset/standard/StandardRule {
184+
public fun <init> ()V
185+
public fun beforeFirstNode (Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfig;)V
186+
public fun beforeVisitChildNodes (Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;Lkotlin/jvm/functions/Function3;)V
187+
}
188+
189+
public final class com/pinterest/ktlint/ruleset/standard/rules/ContextReceiverListWrappingRuleKt {
190+
public static final fun getCONTEXT_RECEIVER_LIST_WRAPPING_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId;
191+
}
192+
183193
public final class com/pinterest/ktlint/ruleset/standard/rules/ContextReceiverWrappingRule : com/pinterest/ktlint/ruleset/standard/StandardRule {
184194
public fun <init> ()V
185195
public fun beforeFirstNode (Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfig;)V

ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.pinterest.ktlint.ruleset.standard.rules.ClassSignatureRule
1818
import com.pinterest.ktlint.ruleset.standard.rules.CommentSpacingRule
1919
import com.pinterest.ktlint.ruleset.standard.rules.CommentWrappingRule
2020
import com.pinterest.ktlint.ruleset.standard.rules.ConditionWrappingRule
21+
import com.pinterest.ktlint.ruleset.standard.rules.ContextReceiverListWrappingRule
2122
import com.pinterest.ktlint.ruleset.standard.rules.ContextReceiverWrappingRule
2223
import com.pinterest.ktlint.ruleset.standard.rules.DiscouragedCommentLocationRule
2324
import com.pinterest.ktlint.ruleset.standard.rules.EnumEntryNameCaseRule
@@ -121,6 +122,7 @@ public class StandardRuleSetProvider : RuleSetProviderV3(RuleSetId.STANDARD) {
121122
RuleProvider { CommentWrappingRule() },
122123
RuleProvider { ConditionWrappingRule() },
123124
RuleProvider { ContextReceiverWrappingRule() },
125+
RuleProvider { ContextReceiverListWrappingRule() },
124126
RuleProvider { DiscouragedCommentLocationRule() },
125127
RuleProvider { EnumEntryNameCaseRule() },
126128
RuleProvider { EnumWrappingRule() },
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package com.pinterest.ktlint.ruleset.standard.rules
2+
3+
import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision
4+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.CONTEXT_RECEIVER
5+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.CONTEXT_RECEIVER_LIST
6+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUN
7+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUNCTION_TYPE
8+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.GT
9+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.RPAR
10+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_ARGUMENT_LIST
11+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_PROJECTION
12+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_REFERENCE
13+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER
14+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER_LIST
15+
import com.pinterest.ktlint.rule.engine.core.api.IndentConfig
16+
import com.pinterest.ktlint.rule.engine.core.api.IndentConfig.Companion.DEFAULT_INDENT_CONFIG
17+
import com.pinterest.ktlint.rule.engine.core.api.RuleId
18+
import com.pinterest.ktlint.rule.engine.core.api.SinceKtlint
19+
import com.pinterest.ktlint.rule.engine.core.api.SinceKtlint.Status.STABLE
20+
import com.pinterest.ktlint.rule.engine.core.api.children20
21+
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig
22+
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY
23+
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY
24+
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.MAX_LINE_LENGTH_PROPERTY
25+
import com.pinterest.ktlint.rule.engine.core.api.firstChildLeafOrSelf20
26+
import com.pinterest.ktlint.rule.engine.core.api.ifAutocorrectAllowed
27+
import com.pinterest.ktlint.rule.engine.core.api.indent20
28+
import com.pinterest.ktlint.rule.engine.core.api.indentWithoutNewlinePrefix
29+
import com.pinterest.ktlint.rule.engine.core.api.isPartOf
30+
import com.pinterest.ktlint.rule.engine.core.api.isPartOfComment20
31+
import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline20
32+
import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithoutNewline20
33+
import com.pinterest.ktlint.rule.engine.core.api.lastChildLeafOrSelf20
34+
import com.pinterest.ktlint.rule.engine.core.api.nextLeaf
35+
import com.pinterest.ktlint.rule.engine.core.api.parent
36+
import com.pinterest.ktlint.rule.engine.core.api.prevLeaf
37+
import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceAfterMe
38+
import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceBeforeMe
39+
import com.pinterest.ktlint.ruleset.standard.StandardRule
40+
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
41+
42+
/**
43+
* Wrapping of context receiver list to a separate line.
44+
*
45+
* IMPORTANT: This rule only affects a context receiver list that does not contain a context receiver. Context receivers are deprecated
46+
* since Kotlin 2.2.0, and are wrapped by the 'context-receiver-wrapping' rule.
47+
*/
48+
@SinceKtlint("1.7", STABLE)
49+
public class ContextReceiverListWrappingRule :
50+
StandardRule(
51+
id = "context-receiver-list-wrapping",
52+
usesEditorConfigProperties =
53+
setOf(
54+
INDENT_SIZE_PROPERTY,
55+
INDENT_STYLE_PROPERTY,
56+
MAX_LINE_LENGTH_PROPERTY,
57+
),
58+
) {
59+
private var indentConfig = DEFAULT_INDENT_CONFIG
60+
private var maxLineLength = MAX_LINE_LENGTH_PROPERTY.defaultValue
61+
62+
override fun beforeFirstNode(editorConfig: EditorConfig) {
63+
indentConfig =
64+
IndentConfig(
65+
indentStyle = editorConfig[INDENT_STYLE_PROPERTY],
66+
tabWidth = editorConfig[INDENT_SIZE_PROPERTY],
67+
)
68+
maxLineLength = editorConfig.maxLineLength()
69+
}
70+
71+
override fun beforeVisitChildNodes(
72+
node: ASTNode,
73+
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision,
74+
) {
75+
when {
76+
node.elementType == CONTEXT_RECEIVER_LIST && node.isContextParameter() -> {
77+
visitContextReceiverList(node, emit)
78+
}
79+
80+
node.elementType == TYPE_ARGUMENT_LIST && node.isContextParameter() -> {
81+
visitContextReceiverTypeArgumentList(node, emit)
82+
}
83+
}
84+
}
85+
86+
private fun ASTNode.isContextParameter(): Boolean = isPartOf(CONTEXT_RECEIVER_LIST) && findChildByType(CONTEXT_RECEIVER) == null
87+
88+
private fun visitContextReceiverList(
89+
node: ASTNode,
90+
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision,
91+
) {
92+
// Context receiver must be followed by new line or comment unless it is a type reference of a parameter
93+
node
94+
.takeUnless { it.isTypeReferenceParameterInFunction() }
95+
?.lastChildLeafOrSelf20
96+
?.nextLeaf { !it.isWhiteSpaceWithoutNewline20 && !it.isPartOfComment20 }
97+
?.takeIf { !it.isWhiteSpaceWithNewline20 }
98+
?.let { nodeAfterContextReceiver ->
99+
emit(nodeAfterContextReceiver.startOffset, "Expected a newline after the context parameter", true)
100+
.ifAutocorrectAllowed {
101+
nodeAfterContextReceiver
102+
.firstChildLeafOrSelf20
103+
.upsertWhitespaceBeforeMe(indentConfig.parentIndentOf(node))
104+
}
105+
}
106+
107+
// Check line length assuming that the context receiver is indented correctly. Wrapping rule must however run before indenting.
108+
if (!node.textContains('\n') &&
109+
node.indentWithoutNewlinePrefix.length + node.textLength > maxLineLength
110+
) {
111+
node
112+
.children20
113+
.filter { it.elementType == VALUE_PARAMETER }
114+
.forEach {
115+
emit(
116+
it.startOffset,
117+
"Newline expected before context parameter as max line length is violated",
118+
true,
119+
).ifAutocorrectAllowed {
120+
it
121+
.prevLeaf
122+
?.upsertWhitespaceAfterMe(indentConfig.childIndentOf(node))
123+
}
124+
}
125+
node
126+
.findChildByType(RPAR)
127+
?.let { rpar ->
128+
emit(
129+
rpar.startOffset,
130+
"Newline expected before closing parenthesis as max line length is violated",
131+
true,
132+
).ifAutocorrectAllowed {
133+
rpar.upsertWhitespaceBeforeMe(node.indent20)
134+
}
135+
}
136+
}
137+
}
138+
139+
private fun ASTNode.isTypeReferenceParameterInFunction() =
140+
takeIf { it.elementType == CONTEXT_RECEIVER_LIST }
141+
?.parent
142+
?.takeIf { it.elementType == FUNCTION_TYPE }
143+
?.parent
144+
?.takeIf { it.elementType == TYPE_REFERENCE }
145+
?.parent
146+
?.takeIf { it.elementType == VALUE_PARAMETER }
147+
?.parent
148+
?.takeIf { it.elementType == VALUE_PARAMETER_LIST }
149+
?.parent
150+
?.let { it.elementType == FUN }
151+
?: false
152+
153+
private fun visitContextReceiverTypeArgumentList(
154+
node: ASTNode,
155+
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision,
156+
) {
157+
val contextReceiverText = node.parent?.text.orEmpty()
158+
// Check line length assuming that the context receiver is indented correctly. Wrapping rule must however run
159+
// before indenting.
160+
if (!contextReceiverText.contains('\n') &&
161+
node.indentWithoutNewlinePrefix.length + contextReceiverText.length > maxLineLength
162+
) {
163+
node
164+
.children20
165+
.filter { it.elementType == TYPE_PROJECTION }
166+
.forEach {
167+
emit(
168+
it.startOffset,
169+
"Newline expected before context parameter type projection as max line length is violated",
170+
true,
171+
).ifAutocorrectAllowed {
172+
it.upsertWhitespaceBeforeMe(indentConfig.childIndentOf(node))
173+
}
174+
}
175+
node
176+
.findChildByType(GT)
177+
?.let { gt ->
178+
emit(
179+
gt.startOffset,
180+
"Newline expected before closing angle bracket as max line length is violated",
181+
true,
182+
).ifAutocorrectAllowed {
183+
// Ideally, the closing angle bracket should be de-indented to make it consistent with
184+
// de-indentation of closing ")", "}" and "]". This however would be inconsistent with how the
185+
// type argument lists are formatted by IntelliJ IDEA default formatter.
186+
gt.upsertWhitespaceBeforeMe(indentConfig.childIndentOf(node))
187+
}
188+
}
189+
}
190+
}
191+
}
192+
193+
public val CONTEXT_RECEIVER_LIST_WRAPPING_RULE_ID: RuleId = ContextReceiverListWrappingRule().ruleId

0 commit comments

Comments
 (0)