Skip to content

Commit 0264e27

Browse files
Add configuration option to handle (certain) annotation with parameters identical to annotations without parameters (#2950)
* Add configuration option to handle (certain) annotation with parameters identical to annotations without parameters Closes #2852 * Fix api contract
1 parent 5dfeebe commit 0264e27

File tree

4 files changed

+216
-50
lines changed

4 files changed

+216
-50
lines changed

documentation/snapshot/docs/rules/standard.md

+4
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ Multiple annotations should be on a separate line than the annotated declaration
5050
}
5151
```
5252

53+
| Configuration setting | ktlint_official | intellij_idea | android_studio |
54+
|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------:|:-------------:|:--------------:|
55+
| `ktlint_annotation_handle_annotations_with_parameters_same_as_annotations_without_parameters`<br/><i>Handle listed annotations identical to annotations without parameters. Value is a comma separated list of names without the `@` prefix. Use `*` for all annotations with parameters.</i> | `unset` | `unset` | `unset` |
56+
5357
Rule id: `standard:annotation`
5458

5559
Suppress or disable rule (1)

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

+5
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,16 @@ public final class com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider
1111
}
1212

1313
public final class com/pinterest/ktlint/ruleset/standard/rules/AnnotationRule : com/pinterest/ktlint/ruleset/standard/StandardRule {
14+
public static final field Companion Lcom/pinterest/ktlint/ruleset/standard/rules/AnnotationRule$Companion;
1415
public fun <init> ()V
1516
public fun beforeFirstNode (Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfig;)V
1617
public fun beforeVisitChildNodes (Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;Lkotlin/jvm/functions/Function3;)V
1718
}
1819

20+
public final class com/pinterest/ktlint/ruleset/standard/rules/AnnotationRule$Companion {
21+
public final fun getANNOTATIONS_WITH_PARAMETERS_NOT_TO_BE_WRAPPED_PROPERTY ()Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfigProperty;
22+
}
23+
1924
public final class com/pinterest/ktlint/ruleset/standard/rules/AnnotationRuleKt {
2025
public static final fun getANNOTATION_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId;
2126
}

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

+66-14
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATION_ENTRY
77
import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATION_TARGET
88
import com.pinterest.ktlint.rule.engine.core.api.ElementType.CLASS
99
import com.pinterest.ktlint.rule.engine.core.api.ElementType.COLON
10+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.CONSTRUCTOR_CALLEE
1011
import com.pinterest.ktlint.rule.engine.core.api.ElementType.CONSTRUCTOR_KEYWORD
1112
import com.pinterest.ktlint.rule.engine.core.api.ElementType.FILE_ANNOTATION_LIST
1213
import com.pinterest.ktlint.rule.engine.core.api.ElementType.GT
14+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.IDENTIFIER
1315
import com.pinterest.ktlint.rule.engine.core.api.ElementType.MODIFIER_LIST
16+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.REFERENCE_EXPRESSION
1417
import com.pinterest.ktlint.rule.engine.core.api.ElementType.RPAR
1518
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_ARGUMENT_LIST
1619
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_PROJECTION
1720
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_REFERENCE
21+
import com.pinterest.ktlint.rule.engine.core.api.ElementType.USER_TYPE
1822
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT
1923
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT_LIST
2024
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER
@@ -27,7 +31,9 @@ import com.pinterest.ktlint.rule.engine.core.api.SinceKtlint.Status.STABLE
2731
import com.pinterest.ktlint.rule.engine.core.api.children
2832
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY
2933
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue
34+
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CommaSeparatedListValueParser
3035
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig
36+
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty
3137
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY
3238
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY
3339
import com.pinterest.ktlint.rule.engine.core.api.firstChildLeafOrSelf
@@ -45,6 +51,7 @@ import com.pinterest.ktlint.rule.engine.core.api.prevLeaf
4551
import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceAfterMe
4652
import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceBeforeMe
4753
import com.pinterest.ktlint.ruleset.standard.StandardRule
54+
import org.ec4j.core.model.PropertyType
4855
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
4956
import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget
5057
import org.jetbrains.kotlin.psi.psiUtil.siblings
@@ -88,6 +95,7 @@ public class AnnotationRule :
8895
setOf(
8996
INDENT_SIZE_PROPERTY,
9097
INDENT_STYLE_PROPERTY,
98+
ANNOTATIONS_WITH_PARAMETERS_NOT_TO_BE_WRAPPED_PROPERTY,
9199
),
92100
visitorModifiers =
93101
setOf(
@@ -99,6 +107,13 @@ public class AnnotationRule :
99107
) {
100108
private var codeStyle = CODE_STYLE_PROPERTY.defaultValue
101109
private var indentConfig = IndentConfig.DEFAULT_INDENT_CONFIG
110+
private var annotationsWithParametersNoToBeWrapped =
111+
ANNOTATIONS_WITH_PARAMETERS_NOT_TO_BE_WRAPPED_PROPERTY.defaultValue
112+
113+
private val handleAllAnnotationsWithParametersSameAsAnnotationsWithoutParameters =
114+
lazy {
115+
annotationsWithParametersNoToBeWrapped.contains("*")
116+
}
102117

103118
override fun beforeFirstNode(editorConfig: EditorConfig) {
104119
codeStyle = editorConfig[CODE_STYLE_PROPERTY]
@@ -107,6 +122,8 @@ public class AnnotationRule :
107122
indentStyle = editorConfig[INDENT_STYLE_PROPERTY],
108123
tabWidth = editorConfig[INDENT_SIZE_PROPERTY],
109124
)
125+
annotationsWithParametersNoToBeWrapped =
126+
editorConfig[ANNOTATIONS_WITH_PARAMETERS_NOT_TO_BE_WRAPPED_PROPERTY]
110127
}
111128

112129
override fun beforeVisitChildNodes(
@@ -165,7 +182,7 @@ public class AnnotationRule :
165182
.children()
166183
.filter { it.elementType == ANNOTATION_ENTRY }
167184
.filter {
168-
it.isAnnotationEntryWithValueArgumentList() ||
185+
it.isAnnotationEntryWithValueArgumentListThatShouldBeWrapped() ||
169186
!it.isPrecededByOtherAnnotationEntryWithoutParametersOnTheSameLine()
170187
}.forEachIndexed { index, annotationEntry ->
171188
annotationEntry
@@ -198,9 +215,9 @@ public class AnnotationRule :
198215

199216
node
200217
.children()
201-
.last { it.elementType == ANNOTATION_ENTRY }
202-
.lastChildLeafOrSelf()
203-
.nextCodeLeaf()
218+
.lastOrNull { it.elementType == ANNOTATION_ENTRY }
219+
?.lastChildLeafOrSelf()
220+
?.nextCodeLeaf()
204221
?.prevLeaf()
205222
?.let { prevLeaf ->
206223
// Let the indentation rule determine the exact indentation and only report and fix when the line needs to be wrapped
@@ -238,7 +255,7 @@ public class AnnotationRule :
238255
require(elementType in ANNOTATION_CONTAINER)
239256
return children()
240257
.any {
241-
it.isAnnotationEntryWithValueArgumentList() &&
258+
it.isAnnotationEntryWithValueArgumentListThatShouldBeWrapped() &&
242259
it.treeParent.treeParent.elementType != VALUE_PARAMETER &&
243260
it.treeParent.treeParent.elementType != VALUE_ARGUMENT &&
244261
it.isNotReceiverTargetAnnotation()
@@ -350,11 +367,36 @@ public class AnnotationRule :
350367
?.findChildByType(ANNOTATION_TARGET)
351368
?.let { USE_SITE_TARGETS[it.text] }
352369

353-
private fun ASTNode.isAnnotationEntryWithValueArgumentList() = getAnnotationEntryValueArgumentList() != null
370+
private fun ASTNode.isAnnotationEntryWithValueArgumentListThatShouldBeWrapped() =
371+
when {
372+
handleAllAnnotationsWithParametersSameAsAnnotationsWithoutParameters.value -> {
373+
// Do never distinct between annotation with and without parameters
374+
false
375+
}
354376

355-
private fun ASTNode.getAnnotationEntryValueArgumentList() =
356-
takeIf { it.elementType == ANNOTATION_ENTRY }
357-
?.findChildByType(VALUE_ARGUMENT_LIST)
377+
annotationsWithParametersNoToBeWrapped.isEmpty() -> {
378+
// All annotations with parameters should be wrapped as the whitelist is empty
379+
true
380+
}
381+
382+
elementType == ANNOTATION_ENTRY && findChildByType(VALUE_ARGUMENT_LIST) != null -> {
383+
// Only wrap annotation with parameters when it is not on the whitelist
384+
getAnnotationIdentifier() !in annotationsWithParametersNoToBeWrapped
385+
}
386+
387+
else -> {
388+
// Annotation without parameter
389+
false
390+
}
391+
}
392+
393+
private fun ASTNode.getAnnotationIdentifier() =
394+
findChildByType(CONSTRUCTOR_CALLEE)
395+
?.findChildByType(TYPE_REFERENCE)
396+
?.findChildByType(USER_TYPE)
397+
?.findChildByType(REFERENCE_EXPRESSION)
398+
?.findChildByType(IDENTIFIER)
399+
?.text
358400

359401
private fun ASTNode.isLastAnnotationEntry() =
360402
this ==
@@ -364,8 +406,8 @@ public class AnnotationRule :
364406

365407
private fun ASTNode.isPrecededByOtherAnnotationEntryWithoutParametersOnTheSameLine() =
366408
siblings(forward = false)
367-
.takeWhile { !it.isWhiteSpaceWithNewline() && !it.isAnnotationEntryWithValueArgumentList() }
368-
.any { it.elementType == ANNOTATION_ENTRY && !it.isAnnotationEntryWithValueArgumentList() }
409+
.takeWhile { !it.isWhiteSpaceWithNewline() && !it.isAnnotationEntryWithValueArgumentListThatShouldBeWrapped() }
410+
.any { it.elementType == ANNOTATION_ENTRY && !it.isAnnotationEntryWithValueArgumentListThatShouldBeWrapped() }
369411

370412
private fun ASTNode.isPrecededByOtherAnnotationEntryOnTheSameLine() =
371413
siblings(forward = false)
@@ -460,14 +502,24 @@ public class AnnotationRule :
460502
return "\n".plus(indentWithoutNewline)
461503
}
462504

463-
private companion object {
464-
val ANNOTATION_CONTAINER =
505+
public companion object {
506+
private val ANNOTATION_CONTAINER =
465507
listOf(
466508
ANNOTATED_EXPRESSION,
467509
FILE_ANNOTATION_LIST,
468510
MODIFIER_LIST,
469511
)
470-
val USE_SITE_TARGETS = AnnotationUseSiteTarget.entries.associateBy { it.renderName }
512+
private val USE_SITE_TARGETS = AnnotationUseSiteTarget.entries.associateBy { it.renderName }
513+
public val ANNOTATIONS_WITH_PARAMETERS_NOT_TO_BE_WRAPPED_PROPERTY: EditorConfigProperty<Set<String>> =
514+
EditorConfigProperty(
515+
type =
516+
PropertyType.LowerCasingPropertyType(
517+
"ktlint_annotation_handle_annotations_with_parameters_same_as_annotations_without_parameters",
518+
"Handle listed annotations identical to annotations without parameters. Value is a comma separated list of names without the '@' prefix. Use '*' for all annotations with parameters.",
519+
CommaSeparatedListValueParser(),
520+
),
521+
defaultValue = setOf("unset"),
522+
)
471523
}
472524
}
473525

0 commit comments

Comments
 (0)