forked from pinterest/ktlint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
AnnotationRule.kt
301 lines (278 loc) · 12.6 KB
/
AnnotationRule.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
package com.pinterest.ktlint.ruleset.standard
import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.ANNOTATED_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION
import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.FILE_ANNOTATION_LIST
import com.pinterest.ktlint.core.ast.ElementType.TYPE_ARGUMENT_LIST
import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT
import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT_LIST
import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.children
import com.pinterest.ktlint.core.ast.firstChildLeafOrSelf
import com.pinterest.ktlint.core.ast.isPartOf
import com.pinterest.ktlint.core.ast.isPartOfComment
import com.pinterest.ktlint.core.ast.isWhiteSpace
import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline
import com.pinterest.ktlint.core.ast.lastChildLeafOrSelf
import com.pinterest.ktlint.core.ast.lineNumber
import com.pinterest.ktlint.core.ast.nextCodeLeaf
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.nextSibling
import com.pinterest.ktlint.core.ast.prevSibling
import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
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.descriptors.annotations.AnnotationUseSiteTarget
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtScript
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespaceAndComments
import org.jetbrains.kotlin.psi.psiUtil.leaves
import org.jetbrains.kotlin.psi.psiUtil.siblings
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
/**
* Ensures multiple annotations are not on the same line as the annotated declaration. Also ensures that annotations
* with parameters are placed on separate lines.
*
* https://kotlinlang.org/docs/reference/coding-conventions.html#annotation-formatting
*
* @see [AnnotationSpacingRule] for white space rules. Moved since
*/
public class AnnotationRule : Rule("annotation") {
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
when (node.elementType) {
FILE_ANNOTATION_LIST -> {
visitFileAnnotationList(node, emit, autoCorrect)
}
ANNOTATION_ENTRY ->
visitAnnotationEntry(node, emit, autoCorrect)
}
}
private fun visitAnnotationEntry(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean,
) {
require(node.elementType == ANNOTATION_ENTRY)
if (node.isAnnotationEntryWithValueArgumentList() &&
node.treeParent.treeParent.elementType != VALUE_PARAMETER && // fun fn(@Ann("blah") a: String)
node.treeParent.treeParent.elementType != VALUE_ARGUMENT && // fn(@Ann("blah") "42")
!node.isPartOf(TYPE_ARGUMENT_LIST) && // val property: Map<@Ann("blah") String, Int>
node.isNotReceiverTargetAnnotation()
) {
checkForAnnotationWithParameterToBePlacedOnSeparateLine(node, emit, autoCorrect)
}
if ((node.isFollowedByOtherAnnotationEntry() && node.isOnSameLineAsNextAnnotationEntry()) ||
(node.isPrecededByOtherAnnotationEntry() && node.isOnSameLineAsAnnotatedConstruct())
) {
checkForAnnotationToBePlacedOnSeparateLine(node, emit, autoCorrect)
}
if (node.treeParent.elementType != ANNOTATION &&
node.isPrecededByOtherAnnotationEntry() &&
node.isOnSameLineAsAnnotatedConstruct()
) {
checkForMultipleAnnotationsOnSameLineAsAnnotatedConstruct(node, emit, autoCorrect)
}
}
private fun checkForAnnotationWithParameterToBePlacedOnSeparateLine(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean,
) {
if (node.isPrecededByOtherAnnotationEntry() && node.isOnSameLineAsPreviousAnnotationEntry()) {
emit(
node.startOffset,
"Annotation with parameter(s) should be placed on a separate line prior to the annotated construct",
true,
)
if (autoCorrect) {
node
.firstChildLeafOrSelf()
.safeAs<LeafPsiElement>()
?.upsertWhitespaceBeforeMe(" ")
}
}
if (node.isOnSameLineAsNextAnnotationEntryOrAnnotatedConstruct()) {
emit(
node.startOffset,
"Annotation with parameter(s) should be placed on a separate line prior to the annotated construct",
// Annotated expressions for which the annotation contains a parameter can be hard to correct
// automatically. See examples below. For now, let them be fixed manually.
// fun foo1() = @Suppress("DEPRECATION") bar()
// if (@Suppress("DEPRECATION") bar()) { .. }
node.treeParent.elementType != ANNOTATED_EXPRESSION,
)
if (autoCorrect) {
node
.lastChildLeafOrSelf()
.nextLeaf()
.safeAs<LeafPsiElement>()
?.let {
if (it.elementType == WHITE_SPACE) {
it.replaceWithText(getNewlineWithIndent(node.treeParent))
} else {
it.rawInsertBeforeMe(
PsiWhiteSpaceImpl(getNewlineWithIndent(node.treeParent)),
)
}
}
}
}
}
private fun checkForMultipleAnnotationsOnSameLineAsAnnotatedConstruct(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean,
) {
if (node.isLastAnnotationEntry()) {
val noAnnotationWithParameters =
node
.siblings(forward = false)
.none { it.isAnnotationEntryWithValueArgumentList() }
if (noAnnotationWithParameters) {
emit(
node.treeParent.startOffset,
"Multiple annotations should not be placed on the same line as the annotated construct",
true,
)
if (autoCorrect) {
node
.lastChildLeafOrSelf()
.nextCodeLeaf()
.safeAs<LeafPsiElement>()
?.upsertWhitespaceBeforeMe(getNewlineWithIndent(node.treeParent))
}
}
}
}
private fun checkForAnnotationToBePlacedOnSeparateLine(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean,
) {
val isFollowedWithAnnotationHavingValueArgumentList =
node
.siblings(forward = true)
.any { it.isAnnotationEntryWithValueArgumentList() }
val isPrecededWithAnnotationOnOtherLine =
node
.siblings(forward = false)
.any { it.isWhiteSpaceWithNewline() }
if (isFollowedWithAnnotationHavingValueArgumentList || isPrecededWithAnnotationOnOtherLine) {
emit(
node.startOffset,
"Annotation must be placed on separate line",
true,
)
if (autoCorrect) {
node
.lastChildLeafOrSelf()
.nextLeaf()
.safeAs<LeafPsiElement>()
?.let {
if (it.elementType == WHITE_SPACE) {
it.replaceWithText(getNewlineWithIndent(node.treeParent))
} else {
it.upsertWhitespaceBeforeMe(
getNewlineWithIndent(node.treeParent),
)
}
}
}
}
}
private fun ASTNode.isNotReceiverTargetAnnotation() =
getAnnotationUseSiteTarget() != AnnotationUseSiteTarget.RECEIVER
private fun ASTNode.getAnnotationUseSiteTarget() =
psi
.safeAs<KtAnnotationEntry>()
?.useSiteTarget
?.getAnnotationUseSiteTarget()
private fun ASTNode.isAnnotationEntryWithValueArgumentList() =
getAnnotationEntryValueArgumentList() != null
private fun ASTNode.getAnnotationEntryValueArgumentList() =
takeIf { it.elementType == ANNOTATION_ENTRY }
?.findChildByType(VALUE_ARGUMENT_LIST)
private fun ASTNode.isLastAnnotationEntry() =
treeParent
.children()
.lastOrNull { it.elementType == ANNOTATION_ENTRY }
.let { it == this }
private fun ASTNode.isPrecededByOtherAnnotationEntry() =
siblings(forward = false).any { it.elementType == ANNOTATION_ENTRY }
private fun ASTNode.isOnSameLineAsPreviousAnnotationEntry() =
siblings(forward = false)
.takeWhile { it.elementType != ANNOTATION_ENTRY }
.none { it.isWhiteSpaceWithNewline() }
private fun ASTNode.isFollowedByOtherAnnotationEntry() =
siblings(forward = true).any { it.elementType == ANNOTATION_ENTRY }
private fun ASTNode.isOnSameLineAsNextAnnotationEntry() =
siblings(forward = true)
.takeWhile { it.elementType != ANNOTATION_ENTRY }
.none { it.isWhiteSpaceWithNewline() }
private fun ASTNode.isOnSameLineAsAnnotatedConstruct() =
lastChildLeafOrSelf()
.leaves(forward = true)
.takeWhile { it.isWhiteSpace() || it.isPartOfComment() }
.none { it.isWhiteSpaceWithNewline() }
private fun ASTNode.isOnSameLineAsNextAnnotationEntryOrAnnotatedConstruct() =
if (isFollowedByOtherAnnotationEntry()) {
isOnSameLineAsNextAnnotationEntry()
} else {
isOnSameLineAsAnnotatedConstruct()
}
private fun visitFileAnnotationList(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean,
) {
val lineNumber = node.lineNumber()
val next = node.nextSibling { it.textLength > 0 }?.let { next ->
val psi = next.psi
((psi as? KtScript)?.blockExpression?.firstChildNode ?: next).nextSibling {
!it.isWhiteSpace() && !(it.isPartOfComment() && it.lineNumber() == lineNumber)
}
}
val nextLineNumber = next?.lineNumber()
if (lineNumber != null && nextLineNumber != null) {
val diff = nextLineNumber - lineNumber
if (diff < 2) {
val psi = node.psi
emit(
psi.endOffset - 1,
"File annotations should be separated from file contents with a blank line",
true,
)
if (autoCorrect) {
if (diff == 0) {
psi.getNextSiblingIgnoringWhitespaceAndComments(withItself = false)?.node
?.prevSibling { it.isWhiteSpace() }
?.let { (it as? LeafPsiElement)?.delete() }
next.treeParent.addChild(PsiWhiteSpaceImpl("\n"), next)
}
next.treeParent.addChild(PsiWhiteSpaceImpl("\n"), next)
}
}
}
}
private fun getNewlineWithIndent(modifierListRoot: ASTNode): String {
val nodeBeforeAnnotations = modifierListRoot.treeParent.treePrev as? PsiWhiteSpace
// If there is no whitespace before the annotation, the annotation is the first
// text in the file
val newLineWithIndent = nodeBeforeAnnotations?.text ?: "\n"
return if (newLineWithIndent.contains('\n')) {
// Make sure we only insert a single newline
newLineWithIndent.substring(newLineWithIndent.lastIndexOf('\n'))
} else {
newLineWithIndent
}
}
}