aboutsummaryrefslogtreecommitdiff
path: root/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoMultipleSpacesRule.kt
blob: 36cf26fe085a0e87edf764748920af355a1ff454 (plain)
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
package com.github.shyiko.ktlint.ruleset.standard

import com.github.shyiko.ktlint.core.Rule
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.com.intellij.psi.PsiComment
import org.jetbrains.kotlin.com.intellij.psi.PsiFile
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.util.PsiTreeUtil
import org.jetbrains.kotlin.diagnostics.DiagnosticUtils
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes

typealias Offset = Int

class NoMultipleSpacesRule : Rule("no-multi-spaces") {

    data class CommentRelativeLocation(
        val prevCommentOffset: Offset,
        val line: Int,
        val column: Int,
        val nextCommentOffset: Offset
    )

    private lateinit var fileNode: ASTNode

    // todo: do not recalculate tree below node.startOffset
    private val commentMap: Map<Offset, CommentRelativeLocation>
        get() {
            val comments = mutableListOf<PsiComment>()
            fileNode.visit { node ->
                val psi = node.psi
                if (psi is PsiComment) { comments.add(psi) }
            }
            return comments.foldIndexed(mutableMapOf<Offset, CommentRelativeLocation>()) { i, acc, comment ->
                val pos = DiagnosticUtils.getLineAndColumnInPsiFile(fileNode.psi as PsiFile,
                    TextRange(comment.startOffset, comment.startOffset))
                acc.put(comment.startOffset, CommentRelativeLocation(
                    prevCommentOffset = comments.getOrNull(i - 1)?.startOffset ?: -1,
                    line = pos.line,
                    column = pos.column,
                    nextCommentOffset = comments.getOrNull(i + 1)?.startOffset ?: -1
                ))
                acc
            }
        }

    override fun visit(node: ASTNode, autoCorrect: Boolean,
            emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) {
        if (node.elementType == KtStubElementTypes.FILE) {
            fileNode = node
        } else
        if (node is PsiWhiteSpace && !node.textContains('\n') && node.getTextLength() > 1) {
            val nextLeaf = PsiTreeUtil.nextLeaf(node, true)
            if (nextLeaf is PsiComment) {
                val positionMap = commentMap
                val commentRL = commentMap[nextLeaf.startOffset]!! // NPE here (or anywhere below) would mean that TARFU
                if (commentRL.prevCommentOffset != -1) {
                    val prevCommentRL = positionMap[commentRL.prevCommentOffset]!!
                    if (commentRL.line - 1 == prevCommentRL.line && commentRL.column == prevCommentRL.column) {
                        return
                    }
                }
                if (commentRL.nextCommentOffset != -1) {
                    val nextCommentRL = positionMap[commentRL.nextCommentOffset]!!
                    if (commentRL.line + 1 == nextCommentRL.line && commentRL.column == nextCommentRL.column) {
                        return
                    }
                }
            }
            emit(node.startOffset + 1, "Unnecessary space(s)", true)
            if (autoCorrect) {
                (node as LeafPsiElement).replaceWithText(" ")
            }
        }
    }

    private fun ASTNode.visit(cb: (node: ASTNode) -> Unit) {
        cb(this)
        this.getChildren(null).forEach { it.visit(cb) }
    }
}