Skip to content

Commit e273989

Browse files
authored
Added logic for parameters and supertypes lists to NewlinesRule (#431)
### What's done: * Changed logic * Added tests * Updated code style guide
1 parent f3da291 commit e273989

File tree

10 files changed

+239
-22
lines changed

10 files changed

+239
-22
lines changed

diktat-analysis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,8 @@
192192
configuration: {}
193193
- name: WRONG_NEWLINES
194194
enabled: true
195-
configuration: {}
195+
configuration:
196+
maxParametersInOneLine: 2
196197
- name: TOO_MANY_CONSECUTIVE_SPACES
197198
enabled: true
198199
configuration:

diktat-rules/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
<groupId>org.cqfn.diktat</groupId>
1111
<artifactId>diktat-parent</artifactId>
1212
<version>0.1.3-SNAPSHOT</version>
13-
1413
</parent>
14+
1515
<properties>
1616
<maven.compiler.source>1.8</maven.compiler.source>
1717
<maven.compiler.target>1.8</maven.compiler.target>

diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/files/NewlinesRule.kt

Lines changed: 94 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.pinterest.ktlint.core.ast.ElementType.BLOCK
88
import com.pinterest.ktlint.core.ast.ElementType.BLOCK_COMMENT
99
import com.pinterest.ktlint.core.ast.ElementType.CALLABLE_REFERENCE_EXPRESSION
1010
import com.pinterest.ktlint.core.ast.ElementType.CALL_EXPRESSION
11+
import com.pinterest.ktlint.core.ast.ElementType.CLASS
1112
import com.pinterest.ktlint.core.ast.ElementType.COLON
1213
import com.pinterest.ktlint.core.ast.ElementType.COLONCOLON
1314
import com.pinterest.ktlint.core.ast.ElementType.COMMA
@@ -21,6 +22,8 @@ import com.pinterest.ktlint.core.ast.ElementType.EOL_COMMENT
2122
import com.pinterest.ktlint.core.ast.ElementType.EQ
2223
import com.pinterest.ktlint.core.ast.ElementType.FUN
2324
import com.pinterest.ktlint.core.ast.ElementType.FUNCTION_LITERAL
25+
import com.pinterest.ktlint.core.ast.ElementType.FUNCTION_TYPE
26+
import com.pinterest.ktlint.core.ast.ElementType.FUNCTION_TYPE_RECEIVER
2427
import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
2528
import com.pinterest.ktlint.core.ast.ElementType.IF
2629
import com.pinterest.ktlint.core.ast.ElementType.IMPORT_DIRECTIVE
@@ -37,46 +40,44 @@ import com.pinterest.ktlint.core.ast.ElementType.PACKAGE_DIRECTIVE
3740
import com.pinterest.ktlint.core.ast.ElementType.PLUS
3841
import com.pinterest.ktlint.core.ast.ElementType.PLUSEQ
3942
import com.pinterest.ktlint.core.ast.ElementType.PRIMARY_CONSTRUCTOR
40-
import com.pinterest.ktlint.core.ast.ElementType.REFERENCE_EXPRESSION
4143
import com.pinterest.ktlint.core.ast.ElementType.RETURN
4244
import com.pinterest.ktlint.core.ast.ElementType.RETURN_KEYWORD
4345
import com.pinterest.ktlint.core.ast.ElementType.SAFE_ACCESS
4446
import com.pinterest.ktlint.core.ast.ElementType.SAFE_ACCESS_EXPRESSION
4547
import com.pinterest.ktlint.core.ast.ElementType.SECONDARY_CONSTRUCTOR
4648
import com.pinterest.ktlint.core.ast.ElementType.SEMICOLON
47-
import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT
49+
import com.pinterest.ktlint.core.ast.ElementType.SUPER_TYPE_LIST
4850
import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT_LIST
51+
import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER
4952
import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER_LIST
5053
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
51-
import com.pinterest.ktlint.core.ast.isLeaf
5254
import com.pinterest.ktlint.core.ast.nextCodeSibling
5355
import com.pinterest.ktlint.core.ast.parent
5456
import com.pinterest.ktlint.core.ast.prevCodeSibling
57+
import org.cqfn.diktat.common.config.rules.RuleConfiguration
5558
import org.cqfn.diktat.common.config.rules.RulesConfig
59+
import org.cqfn.diktat.common.config.rules.getRuleConfig
5660
import org.cqfn.diktat.ruleset.constants.Warnings.REDUNDANT_SEMICOLON
5761
import org.cqfn.diktat.ruleset.constants.Warnings.WRONG_NEWLINES
5862
import org.cqfn.diktat.ruleset.utils.appendNewlineMergingWhiteSpace
5963
import org.cqfn.diktat.ruleset.utils.emptyBlockList
6064
import org.cqfn.diktat.ruleset.utils.extractLineOfText
6165
import org.cqfn.diktat.ruleset.utils.findAllNodesWithSpecificType
66+
import org.cqfn.diktat.ruleset.utils.getIdentifierName
6267
import org.cqfn.diktat.ruleset.utils.isBeginByNewline
6368
import org.cqfn.diktat.ruleset.utils.isEol
6469
import org.cqfn.diktat.ruleset.utils.isFollowedByNewline
6570
import org.cqfn.diktat.ruleset.utils.isSingleLineIfElse
6671
import org.cqfn.diktat.ruleset.utils.leaveOnlyOneNewLine
72+
import org.cqfn.diktat.ruleset.utils.log
73+
import org.jetbrains.kotlin.backend.common.onlyIf
6774
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
6875
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
6976
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
7077
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl
71-
import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType
7278
import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet
73-
import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin
74-
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
75-
import org.jetbrains.kotlin.psi.KtExpression
76-
import org.jetbrains.kotlin.psi.KtExpressionImpl
77-
import org.jetbrains.kotlin.psi.KtPostfixExpression
78-
import org.jetbrains.kotlin.psi.KtQualifiedExpression
79-
import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression
79+
import org.jetbrains.kotlin.psi.KtParameterList
80+
import org.jetbrains.kotlin.psi.KtSuperTypeList
8081
import org.jetbrains.kotlin.psi.psiUtil.children
8182
import org.jetbrains.kotlin.psi.psiUtil.parents
8283
import org.jetbrains.kotlin.psi.psiUtil.siblings
@@ -91,6 +92,7 @@ import org.jetbrains.kotlin.psi.psiUtil.siblings
9192
* 6. Ensures that function or constructor name isn't separated from `(` by space or newline
9293
* 7. Ensures that in multiline lambda newline follows arrow or, in case of lambda without explicit parameters, opening brace
9394
* 8. Checks that functions with single `return` are simplified to functions with expression body
95+
* 9. parameter or argument lists and supertype lists that have more than 2 elements should be separated by newlines
9496
*/
9597
@Suppress("ForbiddenComment")
9698
class NewlinesRule(private val configRules: List<RulesConfig>) : Rule("newlines") {
@@ -107,8 +109,11 @@ class NewlinesRule(private val configRules: List<RulesConfig>) : Rule("newlines"
107109
private val dropChainValues = TokenSet.create(EOL_COMMENT, WHITE_SPACE, BLOCK_COMMENT, KDOC)
108110
}
109111

110-
private lateinit var emitWarn: ((offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit)
112+
private val configuration by lazy {
113+
NewlinesRuleConfiguration(configRules.getRuleConfig(WRONG_NEWLINES)?.configuration ?: emptyMap())
114+
}
111115
private var isFixMode: Boolean = false
116+
private lateinit var emitWarn: ((offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit)
112117

113118
override fun visit(node: ASTNode,
114119
autoCorrect: Boolean,
@@ -124,9 +129,13 @@ class NewlinesRule(private val configRules: List<RulesConfig>) : Rule("newlines"
124129
COMMA -> handleComma(node)
125130
BLOCK -> handleLambdaBody(node)
126131
RETURN -> handleReturnStatement(node)
132+
SUPER_TYPE_LIST, VALUE_PARAMETER_LIST -> handleList(node)
127133
}
128134
}
129135

136+
/**
137+
* Check that EOL semicolon is used only in enums
138+
*/
130139
private fun handleSemicolon(node: ASTNode) {
131140
if (node.isEol() && node.treeParent.elementType != ENUM_ENTRY) {
132141
// semicolon at the end of line which is not part of enum members declarations
@@ -213,6 +222,9 @@ class NewlinesRule(private val configRules: List<RulesConfig>) : Rule("newlines"
213222
}
214223
}
215224

225+
/**
226+
* Check that newline is not placed before a comma
227+
*/
216228
private fun handleComma(node: ASTNode) {
217229
val prevNewLine = node
218230
.parent({ it.treePrev != null }, strict = false)
@@ -296,6 +308,69 @@ class NewlinesRule(private val configRules: List<RulesConfig>) : Rule("newlines"
296308
}
297309
}
298310

311+
/**
312+
* Checks that members of [VALUE_PARAMETER_LIST] (list of function parameters at declaration site) are separated with newlines.
313+
* Also checks that entries of [SUPER_TYPE_LIST] are separated by newlines.
314+
*/
315+
private fun handleList(node: ASTNode) {
316+
if (node.elementType == VALUE_PARAMETER_LIST && node.treeParent.elementType.let { it == FUNCTION_TYPE || it == FUNCTION_TYPE_RECEIVER }) {
317+
// do not check other value lists
318+
return
319+
}
320+
321+
val (numEntries, entryType) = when (node.elementType) {
322+
VALUE_PARAMETER_LIST -> (node.psi as KtParameterList).parameters.size to "value parameters"
323+
SUPER_TYPE_LIST -> (node.psi as KtSuperTypeList).entries.size to "supertype list entries"
324+
else -> {
325+
log.warn("Unexpected node element type ${node.elementType}")
326+
return
327+
}
328+
}
329+
if (numEntries > configuration.maxParametersInOneLine) {
330+
when (node.elementType) {
331+
VALUE_PARAMETER_LIST -> handleFirstValueParameter(node)
332+
}
333+
334+
handleValueParameterList(node, entryType)
335+
}
336+
}
337+
338+
private fun handleFirstValueParameter(node: ASTNode) = node
339+
.children()
340+
.takeWhile { !it.textContains('\n') }
341+
.filter { it.elementType == VALUE_PARAMETER }
342+
.toList()
343+
.onlyIf({ size > 1 }) {
344+
WRONG_NEWLINES.warnAndFix(configRules, emitWarn, isFixMode, "first parameter should be placed on a separate line " +
345+
"or all other parameters should be aligned with it in declaration of <${node.getParentIdentifier()}>", node.startOffset, node) {
346+
node.appendNewlineMergingWhiteSpace(it.first().treePrev.takeIf { it.elementType == WHITE_SPACE }, it.first())
347+
}
348+
}
349+
350+
private fun handleValueParameterList(node: ASTNode, entryType: String) = node
351+
.children()
352+
.filter {
353+
it.elementType == COMMA &&
354+
!it.treeNext.run { elementType == WHITE_SPACE && textContains('\n') }
355+
}
356+
.toList()
357+
.onlyIf({ isNotEmpty() }) { invalidCommas ->
358+
WRONG_NEWLINES.warnAndFix(configRules, emitWarn, isFixMode,
359+
"$entryType should be placed on different lines in declaration of <${node.getParentIdentifier()}>", node.startOffset, node) {
360+
invalidCommas.forEach { comma ->
361+
val nextWhiteSpace = comma.treeNext.takeIf { it.elementType == WHITE_SPACE }
362+
comma.appendNewlineMergingWhiteSpace(nextWhiteSpace, nextWhiteSpace?.treeNext ?: comma.treeNext)
363+
}
364+
}
365+
}
366+
367+
@Suppress("UnsafeCallOnNullableType")
368+
private fun ASTNode.getParentIdentifier() = when (treeParent.elementType) {
369+
PRIMARY_CONSTRUCTOR -> treeParent.treeParent
370+
SECONDARY_CONSTRUCTOR -> parent(CLASS)!!
371+
else -> treeParent
372+
}
373+
.getIdentifierName()?.text
299374

300375
private fun ASTNode.getOrderedCallExpressions(psi: PsiElement, result: MutableList<ASTNode>) {
301376
// if statements here have the only right order - don't change it
@@ -373,3 +448,10 @@ class NewlinesRule(private val configRules: List<RulesConfig>) : Rule("newlines"
373448
firstChildNode.elementType == IDENTIFIER &&
374449
treeParent.elementType == BINARY_EXPRESSION
375450
}
451+
452+
private class NewlinesRuleConfiguration(config: Map<String, String>) : RuleConfiguration(config) {
453+
/**
454+
* If the number of parameters on one line is more than this threshold, all parameters should be placed on separate lines.
455+
*/
456+
val maxParametersInOneLine = config["maxParametersInOneLine"]?.toInt() ?: 2
457+
}

diktat-rules/src/main/resources/diktat-analysis-huawei.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@
191191
configuration: {}
192192
- name: WRONG_NEWLINES
193193
enabled: true
194-
configuration: {}
194+
configuration:
195+
maxParametersInOneLine: 2
195196
- name: TOO_MANY_CONSECUTIVE_SPACES
196197
enabled: true
197198
configuration:

diktat-rules/src/main/resources/diktat-analysis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,8 @@
193193
configuration: {}
194194
- name: WRONG_NEWLINES
195195
enabled: true
196-
configuration: {}
196+
configuration:
197+
maxParametersInOneLine: 2
197198
- name: TOO_MANY_CONSECUTIVE_SPACES
198199
enabled: true
199200
configuration:

diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/files/NewlinesRuleFixTest.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,10 @@ class NewlinesRuleFixTest : FixTestBase("test/paragraph3/newlines", ::NewlinesRu
4848
fun `should replace functions with only return with expression body`() {
4949
fixAndCompare("ExpressionBodyExpected.kt", "ExpressionBodyTest.kt")
5050
}
51+
52+
@Test
53+
@Tag(WarningNames.WRONG_NEWLINES)
54+
fun `should insert newlines in a long parameter or supertype list`() {
55+
fixAndCompare("ParameterListExpected.kt", "ParameterListTest.kt")
56+
}
5157
}

diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/files/NewlinesRuleWarnTest.kt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import org.junit.jupiter.api.Disabled
1111
import org.junit.jupiter.api.Tag
1212
import org.junit.jupiter.api.Test
1313

14+
@Suppress("LargeClass")
1415
class NewlinesRuleWarnTest : LintTestBase(::NewlinesRule) {
1516
private val ruleId = "$DIKTAT_RULE_SET_ID:newlines"
1617
private val shouldBreakAfter = "${WRONG_NEWLINES.warnText()} should break a line after and not before"
@@ -579,4 +580,75 @@ class NewlinesRuleWarnTest : LintTestBase(::NewlinesRule) {
579580
LintError(19, 20, ruleId,"${WRONG_NEWLINES.warnText()} should follow functional style at .", true)
580581
)
581582
}
583+
584+
@Test
585+
@Tag(WarningNames.WRONG_NEWLINES)
586+
fun `should suggest newlines in a long argument list of a constructor`() {
587+
lintMethod(
588+
"""
589+
|class Foo(val arg1: Int, arg2: Int) { }
590+
|
591+
|class Foo(val arg1: Int, arg2: Int, arg3: Int) {
592+
| constructor(arg1: Int, arg2: String, arg3: String) : this(arg1, 0, 0) { }
593+
|}
594+
|
595+
|class Foo(val arg1: Int,
596+
| var arg2: Int,
597+
| arg3: Int) { }
598+
""".trimMargin(),
599+
LintError(3, 10, ruleId, "${WRONG_NEWLINES.warnText()} first parameter should be placed on a separate line or all other parameters " +
600+
"should be aligned with it in declaration of <Foo>", true),
601+
LintError(3, 10, ruleId, "${WRONG_NEWLINES.warnText()} value parameters should be placed on different lines in declaration of <Foo>", true),
602+
LintError(4, 16, ruleId, "${WRONG_NEWLINES.warnText()} first parameter should be placed on a separate line or all other parameters " +
603+
"should be aligned with it in declaration of <Foo>", true),
604+
LintError(4, 16, ruleId, "${WRONG_NEWLINES.warnText()} value parameters should be placed on different lines in declaration of <Foo>", true)
605+
)
606+
}
607+
608+
@Test
609+
@Tag(WarningNames.WRONG_NEWLINES)
610+
fun `should suggest newlines in a long argument list`() {
611+
lintMethod(
612+
"""
613+
|fun foo(arg1: Int, arg2: Int) { }
614+
|
615+
|fun bar(arg1: Int, arg2: Int, arg3: Int) { }
616+
|
617+
|// should not trigger on functional types
618+
|fun bar(arg1: (_arg1: Int, _arg2: Int, _arg3: Int) -> Int) { }
619+
|
620+
|// should not trigger on functional type receivers
621+
|fun bar(arg1: Foo.(_arg1: Int, _arg2: Int, _arg3: Int) -> Int) { }
622+
|
623+
|fun baz(arg1: Int,
624+
| arg2: Int,
625+
| arg3: Int) { }
626+
""".trimMargin(),
627+
LintError(3, 8, ruleId, "${WRONG_NEWLINES.warnText()} first parameter should be placed on a separate line or all other parameters " +
628+
"should be aligned with it in declaration of <bar>", true),
629+
LintError(3, 8, ruleId, "${WRONG_NEWLINES.warnText()} value parameters should be placed on different lines in declaration of <bar>", true)
630+
)
631+
}
632+
633+
@Test
634+
@Tag(WarningNames.WRONG_NEWLINES)
635+
fun `should suggest newlines in a long supertype list`() {
636+
lintMethod(
637+
"""
638+
|class Foo :
639+
| FooBase<Bar>(),
640+
| BazInterface,
641+
| BazSuperclass { }
642+
|
643+
|class Foo : FooBase<Bar>(), BazInterface,
644+
| BazSuperclass { }
645+
|
646+
|class Foo : FooBase<Bar>(), BazInterface, BazSuperclass { }
647+
|
648+
|class Foo : FooBase<Bar>() { }
649+
""".trimMargin(),
650+
LintError(6, 13, ruleId, "${WRONG_NEWLINES.warnText()} supertype list entries should be placed on different lines in declaration of <Foo>", true),
651+
LintError(9, 13, ruleId, "${WRONG_NEWLINES.warnText()} supertype list entries should be placed on different lines in declaration of <Foo>", true)
652+
)
653+
}
582654
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package test.paragraph3.newlines
2+
3+
fun bar(
4+
arg1: Int,
5+
arg2: Int,
6+
arg3: Int) { }
7+
8+
class Foo : FooBase<Bar>(),
9+
BazInterface,
10+
BazSuperclass { }
11+
12+
class Foo(val arg1: Int, arg2: Int) { }
13+
14+
class Foo(
15+
val arg1: Int,
16+
arg2: Int,
17+
arg3: Int) {
18+
constructor(
19+
arg1: Int,
20+
arg2: String,
21+
arg3: String) : this(arg1, 0, 0) { }
22+
}
23+
24+
class Foo(val arg1: Int,
25+
var arg2: Int,
26+
arg3: Int) { }
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package test.paragraph3.newlines
2+
3+
fun bar(arg1: Int, arg2: Int, arg3: Int) { }
4+
5+
class Foo : FooBase<Bar>(), BazInterface, BazSuperclass { }
6+
7+
class Foo(val arg1: Int, arg2: Int) { }
8+
9+
class Foo(val arg1: Int, arg2: Int, arg3: Int) {
10+
constructor(arg1: Int, arg2: String, arg3: String) : this(arg1, 0, 0) { }
11+
}
12+
13+
class Foo(val arg1: Int,
14+
var arg2: Int,
15+
arg3: Int) { }

0 commit comments

Comments
 (0)