Skip to content

Commit b08f9d8

Browse files
authored
Fixing DiktatRuleSet crashes when there are no package and no imports (#464)
Fixing DiktatRuleSet crashes when there are no package and no imports. Issue #267 ### What's done: * Fixed logic in rules * Added tests * Added way to disable several rules in smoke test * Handle cases when package and/or imports are missing in FileStructureRule * Code style
1 parent e273989 commit b08f9d8

File tree

14 files changed

+225
-74
lines changed

14 files changed

+225
-74
lines changed

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

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import com.pinterest.ktlint.core.ast.ElementType.IMPORT_LIST
1010
import com.pinterest.ktlint.core.ast.ElementType.KDOC
1111
import com.pinterest.ktlint.core.ast.ElementType.PACKAGE_DIRECTIVE
1212
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
13+
import com.pinterest.ktlint.core.ast.children
14+
import com.pinterest.ktlint.core.ast.isPartOfComment
15+
import com.pinterest.ktlint.core.ast.isWhiteSpace
16+
import com.pinterest.ktlint.core.ast.nextSibling
17+
import com.pinterest.ktlint.core.ast.prevSibling
1318
import org.cqfn.diktat.common.config.rules.RuleConfiguration
1419
import org.cqfn.diktat.common.config.rules.RulesConfig
1520
import org.cqfn.diktat.common.config.rules.getCommonConfiguration
@@ -21,7 +26,6 @@ import org.cqfn.diktat.ruleset.constants.Warnings.FILE_UNORDERED_IMPORTS
2126
import org.cqfn.diktat.ruleset.constants.Warnings.FILE_WILDCARD_IMPORTS
2227
import org.cqfn.diktat.ruleset.rules.PackageNaming.Companion.PACKAGE_SEPARATOR
2328
import org.cqfn.diktat.ruleset.utils.StandardPlatforms
24-
import org.cqfn.diktat.ruleset.utils.findChildBefore
2529
import org.cqfn.diktat.ruleset.utils.getFileName
2630
import org.cqfn.diktat.ruleset.utils.handleIncorrectOrder
2731
import org.cqfn.diktat.ruleset.utils.moveChildBefore
@@ -43,7 +47,6 @@ import org.jetbrains.kotlin.psi.KtImportDirective
4347
* 5. Ensures there are no wildcard imports
4448
*/
4549
class FileStructureRule(private val configRules: List<RulesConfig>) : Rule("file-structure") {
46-
4750
private lateinit var emitWarn: ((offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit)
4851
private var isFixMode: Boolean = false
4952
private var fileName: String = ""
@@ -87,45 +90,51 @@ class FileStructureRule(private val configRules: List<RulesConfig>) : Rule("file
8790
}
8891

8992
private fun checkCodeBlocksOrderAndEmptyLines(node: ASTNode) {
90-
// fixme handle other elements that could be present before package (other comments)
91-
val copyrightComment = node.findChildBefore(PACKAGE_DIRECTIVE, BLOCK_COMMENT)
92-
val headerKdoc = node.findChildBefore(PACKAGE_DIRECTIVE, KDOC)
93-
val fileAnnotations = node.findChildByType(FILE_ANNOTATION_LIST)
94-
// PACKAGE_DIRECTIVE node is always present in regular kt files and might be absent in kts
95-
// kotlin compiler itself enforces it's position in the file if it is present
96-
// fixme: handle cases when this node is not present
97-
val packageDirectiveNode = (node.psi as KtFile).packageDirective?.node ?: return
98-
// fixme: find cases when node.psi.importLists.size > 1, handle cases when it's not present
99-
val importsList = (node.psi as KtFile).importList?.node ?: return
93+
// PACKAGE_DIRECTIVE node is always present in regular kt files and might be absent in kts.
94+
// Kotlin compiler itself enforces it's position in the file if it is present.
95+
// If package directive is missing in .kt file (default package), the node is still present in the AST.
96+
// fixme: find and handle cases when this node is not present (.kts files?)
97+
val packageDirectiveNode = (node.psi as KtFile).packageDirective?.takeUnless { it.isRoot }?.node
98+
// fixme: find cases when node.psi.importLists.size > 1, handle cases when it's not present (null)
99+
val importsList = (node.psi as KtFile).importList?.takeIf { it.imports.isNotEmpty() }?.node
100+
101+
// this node will be an anchor with respect to which we will look for all other nodes
102+
val firstCodeNode = packageDirectiveNode
103+
?: importsList
104+
?: node.children().firstOrNull {
105+
// taking nodes with actual code
106+
!it.isWhiteSpace() && !it.isPartOfComment() &&
107+
// but not the ones we are going to move
108+
it.elementType != FILE_ANNOTATION_LIST&& it.elementType != IMPORT_LIST &&
109+
// if we are here, then package is default and we don't need to select the empty PACKAGE_DIRECTIVE node
110+
it.elementType != PACKAGE_DIRECTIVE
111+
}
112+
?: return // at this point it means the file contains only comments
113+
// fixme: handle other elements that could be present before package (other comments)
114+
var copyrightComment = firstCodeNode.prevSibling { it.elementType == BLOCK_COMMENT }
115+
var headerKdoc = firstCodeNode.prevSibling { it.elementType == KDOC }
116+
var fileAnnotations = node.findChildByType(FILE_ANNOTATION_LIST)
100117

101118
// checking order
102119
listOfNotNull(copyrightComment, headerKdoc, fileAnnotations).handleIncorrectOrder({
103-
getSiblingBlocks(copyrightComment, headerKdoc, fileAnnotations, packageDirectiveNode)
120+
getSiblingBlocks(copyrightComment, headerKdoc, fileAnnotations, firstCodeNode)
104121
}) { astNode, beforeThisNode ->
105-
FILE_INCORRECT_BLOCKS_ORDER.warnAndFix(
106-
configRules,
107-
emitWarn,
108-
isFixMode,
109-
astNode.text.lines().first(),
110-
astNode.startOffset,
111-
astNode
112-
) {
113-
node.moveChildBefore(astNode, beforeThisNode, true)
122+
FILE_INCORRECT_BLOCKS_ORDER.warnAndFix(configRules, emitWarn, isFixMode, astNode.text.lines().first(), astNode.startOffset, astNode) {
123+
val result = node.moveChildBefore(astNode, beforeThisNode, true)
124+
result.newNodes.first().run {
125+
// reassign values to the nodes that could have been moved
126+
when (elementType) {
127+
BLOCK_COMMENT -> copyrightComment = this
128+
KDOC -> headerKdoc = this
129+
FILE_ANNOTATION_LIST -> fileAnnotations = this
130+
}
131+
}
114132
astNode.treeNext?.let { node.replaceChild(it, PsiWhiteSpaceImpl("\n\n")) }
115133
}
116134
}
117135

118136
// checking empty lines
119-
arrayOf(copyrightComment, headerKdoc, fileAnnotations, packageDirectiveNode, importsList).forEach { astNode ->
120-
astNode?.treeNext?.apply {
121-
if (elementType == WHITE_SPACE && text.count { it == '\n' } != 2) {
122-
FILE_NO_BLANK_LINE_BETWEEN_BLOCKS.warnAndFix(configRules, emitWarn, isFixMode, astNode.text.lines().first(),
123-
astNode.startOffset, astNode) {
124-
(this as LeafPsiElement).replaceWithText("\n\n${text.replace("\n", "")}")
125-
}
126-
}
127-
}
128-
}
137+
insertNewlinesBetweenBlocks(listOf(copyrightComment, headerKdoc, fileAnnotations, packageDirectiveNode, importsList))
129138
}
130139

131140
@Suppress("UnsafeCallOnNullableType")
@@ -186,6 +195,23 @@ class FileStructureRule(private val configRules: List<RulesConfig>) : Rule("file
186195
}
187196
}
188197

198+
private fun insertNewlinesBetweenBlocks(blocks: List<ASTNode?>) {
199+
blocks.forEach { astNode ->
200+
// if package directive is missing, node is still present, but it's text is empty, so we need to check treeNext to get meaningful results
201+
astNode?.nextSibling { it.text.isNotEmpty() }?.apply {
202+
if (elementType == WHITE_SPACE && text.count { it == '\n' } != 2) {
203+
FILE_NO_BLANK_LINE_BETWEEN_BLOCKS.warnAndFix(configRules, emitWarn, isFixMode, astNode.text.lines().first(),
204+
astNode.startOffset, astNode) {
205+
(this as LeafPsiElement).replaceWithText("\n\n${text.replace("\n", "")}")
206+
}
207+
}
208+
}
209+
}
210+
}
211+
212+
/**
213+
* @return a pair of nodes between which [this] node should be placed, i.e. after the first and before the second element
214+
*/
189215
private fun ASTNode.getSiblingBlocks(
190216
copyrightComment: ASTNode?,
191217
headerKdoc: ASTNode?,

diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/kdoc/CommentsFormatting.kt

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.pinterest.ktlint.core.ast.ElementType.THEN
2222
import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT_LIST
2323
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
2424
import com.pinterest.ktlint.core.ast.isWhiteSpace
25+
import com.pinterest.ktlint.core.ast.prevSibling
2526
import org.cqfn.diktat.common.config.rules.RuleConfiguration
2627
import org.cqfn.diktat.common.config.rules.RulesConfig
2728
import org.cqfn.diktat.common.config.rules.getRuleConfig
@@ -38,16 +39,16 @@ import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType
3839
/**
3940
* This class handles rule 2.6
4041
* Part 1:
41-
* there must be 1 space between the comment character and the content of the comment;
42-
* there must be a newline between a Kdoc and the previous code above and no blank lines after.
43-
* No need to add a blank line before a first comment in this particular name space (code block), for example between function declaration and first comment in a function body.
42+
* * there must be 1 space between the comment character and the content of the comment;
43+
* * there must be a newline between a Kdoc and the previous code above and no blank lines after.
44+
* * No need to add a blank line before a first comment in this particular name space (code block), for example between function declaration
45+
* and first comment in a function body.
4446
*
4547
* Part 2:
46-
* Leave one single space between the comment on the right side of the code and the code.
47-
* Comments in if else should be inside code blocks. Exception: General if comment
48+
* * Leave one single space between the comment on the right side of the code and the code.
49+
* * Comments in if else should be inside code blocks. Exception: General if comment
4850
*/
4951
class CommentsFormatting(private val configRules: List<RulesConfig>) : Rule("kdoc-comments-codeblocks-formatting") {
50-
5152
companion object {
5253
private const val MAX_SPACES = 1
5354
private const val APPROPRIATE_COMMENT_SPACES = 1
@@ -65,22 +66,15 @@ class CommentsFormatting(private val configRules: List<RulesConfig>) : Rule("kdo
6566
val configuration = CommentsFormattingConfiguration(
6667
configRules.getRuleConfig(COMMENT_WHITE_SPACE)?.configuration ?: mapOf())
6768

68-
6969
when (node.elementType) {
7070
CLASS, FUN, PROPERTY -> {
7171
checkBlankLineAfterKdoc(node, EOL_COMMENT)
7272
checkBlankLineAfterKdoc(node, KDOC)
7373
checkBlankLineAfterKdoc(node, BLOCK_COMMENT)
7474
}
75-
IF -> {
76-
handleIfElse(node)
77-
}
78-
EOL_COMMENT, BLOCK_COMMENT -> {
79-
handleEolAndBlockComments(node, configuration)
80-
}
81-
KDOC -> {
82-
handleKdocComments(node, configuration)
83-
}
75+
IF -> handleIfElse(node)
76+
EOL_COMMENT, BLOCK_COMMENT -> handleEolAndBlockComments(node, configuration)
77+
KDOC -> handleKdocComments(node, configuration)
8478
}
8579
}
8680

@@ -257,15 +251,14 @@ class CommentsFormatting(private val configRules: List<RulesConfig>) : Rule("kdo
257251

258252
private fun checkClassComment(node: ASTNode) {
259253
if (isFirstComment(node)) {
260-
if (node.isBlockOrClassBody())
254+
if (node.isBlockOrClassBody()) {
261255
checkFirstCommentSpaces(node)
262-
else
256+
} else {
263257
checkFirstCommentSpaces(node.treeParent)
264-
265-
return
266-
}
267-
268-
if (node.treeParent.elementType != FILE && !node.treeParent.treePrev.isWhiteSpace()) {
258+
}
259+
} else if (node.treeParent.elementType != FILE && !node.treeParent.treePrev.isWhiteSpace()) {
260+
// fixme: we might face more issues because newline node is inserted in a wrong place which causes consecutive
261+
// white spaces to be split among nodes on different levels. But this requires investigation.
269262
WRONG_NEWLINES_AROUND_KDOC.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) {
270263
node.treeParent.treeParent.addChild(PsiWhiteSpaceImpl("\n"), node.treeParent)
271264
}
@@ -289,25 +282,26 @@ class CommentsFormatting(private val configRules: List<RulesConfig>) : Rule("kdo
289282
}
290283
}
291284

292-
293285
private fun isFirstComment(node: ASTNode): Boolean {
294-
// In case when comment is inside of a function or class
295-
if (node.isBlockOrClassBody()) {
296-
return if (node.treePrev.isWhiteSpace())
286+
return if (node.isBlockOrClassBody()) {
287+
// In case when comment is inside of a function or class
288+
if (node.treePrev.isWhiteSpace()) {
297289
node.treePrev.treePrev.elementType == LBRACE
298-
else
299-
node.treePrev == null || node.treePrev.elementType == LBRACE // null is handled for functional literal
290+
} else {
291+
node.treePrev == null || node.treePrev.elementType == LBRACE // null is handled for functional literal
292+
}
293+
} else if (node.treeParent?.treeParent?.elementType == FILE && node.treeParent.prevSibling { it.text.isNotBlank() } == null) {
294+
// `treeParent` is the first not-empty node in a file
295+
true
296+
} else if (node.treeParent.elementType != FILE && node.treeParent.treePrev != null &&
297+
node.treeParent.treePrev.treePrev != null) {
298+
// When comment inside of a PROPERTY
299+
node.treeParent.treePrev.treePrev.elementType == LBRACE
300+
} else {
301+
node.treeParent.getAllChildrenWithType(node.elementType).first() == node
300302
}
301-
302-
// When comment inside of a PROPERTY
303-
if (node.treeParent.elementType != FILE && node.treeParent.treePrev != null &&
304-
node.treeParent.treePrev.treePrev != null)
305-
return node.treeParent.treePrev.treePrev.elementType == LBRACE
306-
307-
return node.treeParent.getAllChildrenWithType(node.elementType).first() == node
308303
}
309304

310-
311305
private fun ASTNode.isBlockOrClassBody() : Boolean = treeParent.elementType == BLOCK || treeParent.elementType == CLASS_BODY
312306

313307
class CommentsFormattingConfiguration(config: Map<String, String>) : RuleConfiguration(config) {

diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet
2929
import org.jetbrains.kotlin.psi.KtAnnotationEntry
3030
import org.jetbrains.kotlin.psi.KtClass
3131
import org.jetbrains.kotlin.psi.KtIfExpression
32+
import org.jetbrains.kotlin.psi.psiUtil.children
3233
import org.jetbrains.kotlin.psi.psiUtil.parents
3334
import org.jetbrains.kotlin.psi.psiUtil.siblings
3435
import org.slf4j.Logger
@@ -392,11 +393,10 @@ fun ASTNode.moveChildBefore(
392393
beforeThisNode: ASTNode?,
393394
withNextNode: Boolean = false
394395
): ReplacementResult {
395-
require(childToMove in getChildren(null)) { "can only move child of this node" }
396-
require(beforeThisNode?.let { it in getChildren(null) }
397-
?: true) { "can only place node before another child of this node" }
396+
require(childToMove in children()) { "can only move child of this node" }
397+
require(beforeThisNode == null || beforeThisNode in children()) { "can only place node before another child of this node" }
398398
val movedChild = childToMove.clone() as ASTNode
399-
val nextMovedChild = childToMove.treeNext?.let { it.clone() as ASTNode }?.takeIf { withNextNode }
399+
val nextMovedChild = childToMove.treeNext?.takeIf { withNextNode }?.let { it.clone() as ASTNode }
400400
val nextOldChild = childToMove.treeNext.takeIf { withNextNode && it != null }
401401
addChild(movedChild, beforeThisNode)
402402
if (nextMovedChild != null && nextOldChild != null) {
@@ -450,6 +450,11 @@ fun ASTNode.areChildrenBeforeGroup(children: List<ASTNode>, group: List<ASTNode>
450450
return children.map { getChildren(null).indexOf(it) }.max()!! < group.map { getChildren(null).indexOf(it) }.min()!!
451451
}
452452

453+
/**
454+
* A function that rearranges nodes in a [this] list.
455+
* @param getSiblingBlocks a function which returns nodes that should be before and after the current node
456+
* @param incorrectPositionHandler function that moves the current node with respect to node before which in should be placed
457+
*/
453458
fun List<ASTNode>.handleIncorrectOrder(
454459
getSiblingBlocks: ASTNode.() -> Pair<ASTNode?, ASTNode>,
455460
incorrectPositionHandler: (nodeToMove: ASTNode, beforeThisNode: ASTNode) -> Unit

diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter2/CommentsFormattingFixTest.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,10 @@ class CommentsFormattingFixTest: FixTestBase("test/paragraph2/kdoc/", ::Comments
2929
fun `test example from code style`() {
3030
fixAndCompare("KdocCodeBlockFormattingExampleExpected.kt", "KdocCodeBlockFormattingExampleTest.kt")
3131
}
32+
33+
@Test
34+
@Tag(WRONG_NEWLINES_AROUND_KDOC)
35+
fun `regression - should not insert newline before the first comment in a file`() {
36+
fixAndCompare("NoPackageNoImportExpected.kt", "NoPackageNoImportTest.kt")
37+
}
3238
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,16 @@ class FileStructureRuleTestFix : FixTestBase("test/paragraph3/file_structure", :
6565
)
6666
)
6767
}
68+
69+
@Test
70+
@Tag(WarningNames.FILE_UNORDERED_IMPORTS)
71+
fun `should still work with default package and some imports`() {
72+
fixAndCompare("DefaultPackageWithImportsExpected.kt", "DefaultPackageWithImportsTest.kt")
73+
}
74+
75+
@Test
76+
@Tag(WarningNames.FILE_UNORDERED_IMPORTS)
77+
fun `should still work with default package and no imports`() {
78+
fixAndCompare("NoImportNoPackageExpected.kt", "NoImportNoPackageTest.kt")
79+
}
6880
}

0 commit comments

Comments
 (0)