| 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) } |
| } |
| } |