diff options
Diffstat (limited to 'ktlint-ruleset-standard/src/main/kotlin/com')
29 files changed, 887 insertions, 173 deletions
diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ChainWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ChainWrappingRule.kt new file mode 100644 index 00000000..3c24344d --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ChainWrappingRule.kt @@ -0,0 +1,127 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.Rule +import org.jetbrains.kotlin.KtNodeTypes +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.PsiComment +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +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.com.intellij.psi.tree.TokenSet +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.lexer.KtTokens.ANDAND +import org.jetbrains.kotlin.lexer.KtTokens.DIV +import org.jetbrains.kotlin.lexer.KtTokens.DOT +import org.jetbrains.kotlin.lexer.KtTokens.ELVIS +import org.jetbrains.kotlin.lexer.KtTokens.MINUS +import org.jetbrains.kotlin.lexer.KtTokens.MUL +import org.jetbrains.kotlin.lexer.KtTokens.OROR +import org.jetbrains.kotlin.lexer.KtTokens.PERC +import org.jetbrains.kotlin.lexer.KtTokens.PLUS +import org.jetbrains.kotlin.lexer.KtTokens.SAFE_ACCESS +import org.jetbrains.kotlin.psi.psiUtil.nextLeaf +import org.jetbrains.kotlin.psi.psiUtil.prevLeaf + +class ChainWrappingRule : Rule("chain-wrapping") { + + private val sameLineTokens = TokenSet.create(MUL, DIV, PERC, ANDAND, OROR) + private val prefixTokens = TokenSet.create(PLUS, MINUS) + private val nextLineTokens = TokenSet.create(DOT, SAFE_ACCESS, ELVIS) + private val noSpaceAroundTokens = TokenSet.create(DOT, SAFE_ACCESS) + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + /* + org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement (DOT) | "." + org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl (WHITE_SPACE) | "\n " + org.jetbrains.kotlin.psi.KtCallExpression (CALL_EXPRESSION) + */ + val elementType = node.elementType + if (nextLineTokens.contains(elementType)) { + if (node.psi.isPartOf(PsiComment::class)) { + return + } + val nextLeaf = node.psi.nextLeafIgnoringWhitespaceAndComments()?.prevLeaf(true) + if (nextLeaf is PsiWhiteSpaceImpl && nextLeaf.textContains('\n')) { + emit(node.startOffset, "Line must not end with \"${node.text}\"", true) + if (autoCorrect) { + // rewriting + // <prevLeaf><node="."><nextLeaf="\n"> to + // <prevLeaf><delete space if any><nextLeaf="\n"><node="."><space if needed> + // (or) + // <prevLeaf><node="."><spaceBeforeComment><comment><nextLeaf="\n"> to + // <prevLeaf><delete space if any><spaceBeforeComment><comment><nextLeaf="\n"><node="."><space if needed> + val prevLeaf = node.psi.prevLeaf(true) + if (prevLeaf is PsiWhiteSpaceImpl) { + prevLeaf.node.treeParent.removeChild(prevLeaf.node) + } + if (!noSpaceAroundTokens.contains(elementType)) { + nextLeaf.rawInsertAfterMe(PsiWhiteSpaceImpl(" ")) + } + node.treeParent.removeChild(node) + nextLeaf.rawInsertAfterMe(node.psi as LeafPsiElement) + } + } + } else if (sameLineTokens.contains(elementType) || prefixTokens.contains(elementType)) { + if (node.psi.isPartOf(PsiComment::class)) { + return + } + val prevLeaf = node.psi.prevLeaf(true) + if ( + prevLeaf is PsiWhiteSpaceImpl && + prevLeaf.textContains('\n') && + // fn(*typedArray<...>()) case + (elementType != MUL || !prevLeaf.isPartOfSpread()) && + // unary +/- + (!prefixTokens.contains(elementType) || !node.isInPrefixPosition()) && + // LeafPsiElement->KtOperationReferenceExpression->KtPrefixExpression->KtWhenConditionWithExpression + !node.isPartOfWhenCondition() + ) { + emit(node.startOffset, "Line must not begin with \"${node.text}\"", true) + if (autoCorrect) { + // rewriting + // <insertionPoint><prevLeaf="\n"><node="&&"><nextLeaf=" "> to + // <insertionPoint><prevLeaf=" "><node="&&"><nextLeaf="\n"><delete node="&&"><delete nextLeaf=" "> + // (or) + // <insertionPoint><spaceBeforeComment><comment><prevLeaf="\n"><node="&&"><nextLeaf=" "> to + // <insertionPoint><space if needed><node="&&"><spaceBeforeComment><comment><prevLeaf="\n"><delete node="&&"><delete nextLeaf=" "> + val nextLeaf = node.psi.nextLeaf(true) + if (nextLeaf is PsiWhiteSpaceImpl) { + nextLeaf.node.treeParent.removeChild(nextLeaf.node) + } + val insertionPoint = prevLeaf.prevLeafIgnoringWhitespaceAndComments() as LeafPsiElement + node.treeParent.removeChild(node) + insertionPoint.rawInsertAfterMe(node.psi as LeafPsiElement) + if (!noSpaceAroundTokens.contains(elementType)) { + insertionPoint.rawInsertAfterMe(PsiWhiteSpaceImpl(" ")) + } + } + } + } + } + + private fun PsiElement.isPartOfSpread() = + prevLeafIgnoringWhitespaceAndComments()?.let { leaf -> + val type = leaf.node.elementType + type == KtTokens.LPAR || + type == KtTokens.COMMA || + type == KtTokens.LBRACE || + type == KtTokens.ELSE_KEYWORD || + KtTokens.OPERATIONS.contains(type) + } == true + + private fun ASTNode.isInPrefixPosition() = + treeParent?.treeParent?.elementType == KtNodeTypes.PREFIX_EXPRESSION + + private fun ASTNode.isPartOfWhenCondition() = + treeParent?.treeParent?.treeParent?.elementType == KtNodeTypes.WHEN_CONDITION_EXPRESSION + + private fun PsiElement.nextLeafIgnoringWhitespaceAndComments() = + this.nextLeaf { it.node.elementType != KtTokens.WHITE_SPACE && !it.isPartOf(PsiComment::class) } + + private fun PsiElement.prevLeafIgnoringWhitespaceAndComments() = + this.prevLeaf { it.node.elementType != KtTokens.WHITE_SPACE && !it.isPartOf(PsiComment::class) } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/CommentSpacingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/CommentSpacingRule.kt new file mode 100644 index 00000000..68ba62b0 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/CommentSpacingRule.kt @@ -0,0 +1,35 @@ +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.psi.PsiComment +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.com.intellij.psi.util.PsiTreeUtil + +class CommentSpacingRule : Rule("comment-spacing") { + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node is PsiComment && node is LeafPsiElement && node.getText().startsWith("//")) { + val prevLeaf = PsiTreeUtil.prevLeaf(node) + if (prevLeaf !is PsiWhiteSpace && prevLeaf is LeafPsiElement) { + emit(node.startOffset, "Missing space before //", true) + if (autoCorrect) { + node.rawInsertBeforeMe(PsiWhiteSpaceImpl(" ")) + } + } + val text = node.getText() + if (text.length != 2 && !text.startsWith("// ")) { + emit(node.startOffset, "Missing space after //", true) + if (autoCorrect) { + node.rawReplaceWithText("// " + text.removePrefix("//")) + } + } + } + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/EditorConfig.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/EditorConfig.kt new file mode 100644 index 00000000..64a56484 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/EditorConfig.kt @@ -0,0 +1,40 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.KtLint +import org.jetbrains.kotlin.com.intellij.lang.FileASTNode + +// http://editorconfig.org/ +internal data class EditorConfig( + val indentSize: Int, + val continuationIndentSize: Int, + val maxLineLength: Int, + val insertFinalNewline: Boolean? +) { + + companion object { + + private const val DEFAULT_INDENT = 4 + + // https://android.github.io/kotlin-guides/style.html#line-wrapping + private const val ANDROID_MAX_LINE_LENGTH = 100 + + fun from(node: FileASTNode): EditorConfig { + val editorConfig = node.getUserData(KtLint.EDITOR_CONFIG_USER_DATA_KEY)!! + val indentSizeRaw = editorConfig.get("indent_size") + val indentSize = when { + indentSizeRaw?.toLowerCase() == "unset" -> -1 + else -> indentSizeRaw?.toIntOrNull() ?: DEFAULT_INDENT + } + val continuationIndentSizeRaw = editorConfig.get("continuation_indent_size") + val continuationIndentSize = when { + continuationIndentSizeRaw?.toLowerCase() == "unset" -> -1 + else -> continuationIndentSizeRaw?.toIntOrNull() ?: indentSize + } + val android = node.getUserData(KtLint.ANDROID_USER_DATA_KEY)!! + val maxLineLength = editorConfig.get("max_line_length")?.toIntOrNull() + ?: if (android) ANDROID_MAX_LINE_LENGTH else -1 + val insertFinalNewline = editorConfig.get("insert_final_newline")?.toBoolean() + return EditorConfig(indentSize, continuationIndentSize, maxLineLength, insertFinalNewline) + } + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FilenameRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FilenameRule.kt new file mode 100644 index 00000000..8c7169c8 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FilenameRule.kt @@ -0,0 +1,63 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.KtLint +import com.github.shyiko.ktlint.core.Rule +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespaceAndComments +import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes +import java.nio.file.Paths + +/** + * If there is only one top level class/object/typealias in a given file, then its name should match the file's name. + */ +class FilenameRule : Rule("filename"), Rule.Modifier.RestrictToRoot { + + private val ignoreSet = setOf<IElementType>( + KtStubElementTypes.FILE_ANNOTATION_LIST, + KtStubElementTypes.PACKAGE_DIRECTIVE, + KtStubElementTypes.IMPORT_LIST, + KtTokens.WHITE_SPACE, + KtTokens.EOL_COMMENT, + KtTokens.BLOCK_COMMENT, + KtTokens.DOC_COMMENT, + KtTokens.SHEBANG_COMMENT + ) + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + val filePath = node.getUserData(KtLint.FILE_PATH_USER_DATA_KEY) + if (filePath?.endsWith(".kt") != true) { + // ignore all non ".kt" files (including ".kts") + return + } + var type: String? = null + var className: String? = null + for (el in node.getChildren(null)) { + if (el.elementType == KtStubElementTypes.CLASS || + el.elementType == KtStubElementTypes.OBJECT_DECLARATION || + el.elementType == KtStubElementTypes.TYPEALIAS) { + if (className != null) { + // more than one class/object/typealias present + return + } + val id = el.findChildByType(KtTokens.IDENTIFIER) + type = id?.psi?.getPrevSiblingIgnoringWhitespaceAndComments(false)?.text + className = id?.text + } else if (!ignoreSet.contains(el.elementType)) { + // https://github.com/android/android-ktx/blob/master/src/main/java/androidx/core/graphics/Path.kt case + return + } + } + if (className != null) { + val name = Paths.get(filePath).fileName.toString().substringBefore(".") + if (name != "package" && name != className) { + emit(0, "$type $className should be declared in a file named $className.kt", false) + } + } + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FinalNewlineRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FinalNewlineRule.kt index 67057e6e..93e80290 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FinalNewlineRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FinalNewlineRule.kt @@ -1,13 +1,13 @@ package com.github.shyiko.ktlint.ruleset.standard -import com.github.shyiko.ktlint.core.KtLint import com.github.shyiko.ktlint.core.Rule import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.lang.FileASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes -class FinalNewlineRule : Rule("final-newline") { +class FinalNewlineRule : Rule("final-newline"), Rule.Modifier.RestrictToRoot { override fun visit( node: ASTNode, @@ -15,9 +15,9 @@ class FinalNewlineRule : Rule("final-newline") { emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { if (node.elementType == KtStubElementTypes.FILE) { - val editorConfig = node.getUserData(KtLint.EDITOR_CONFIG_USER_DATA_KEY)!! - val insertFinalNewline = editorConfig.get("insert_final_newline")?.toBoolean() ?: return - val lastNode = node.lastChildNode + val ec = EditorConfig.from(node as FileASTNode) + val insertFinalNewline = ec.insertFinalNewline ?: return + val lastNode = lastChildNodeOf(node) if (insertFinalNewline) { if (lastNode !is PsiWhiteSpace || !lastNode.textContains('\n')) { // (PsiTreeUtil.getDeepestLast(lastNode.psi).node ?: lastNode).startOffset @@ -36,4 +36,7 @@ class FinalNewlineRule : Rule("final-newline") { } } } + + private tailrec fun lastChildNodeOf(node: ASTNode): ASTNode? = + if (node.lastChildNode == null) node else lastChildNodeOf(node.lastChildNode) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/IndentationRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/IndentationRule.kt index 87b833af..d270735d 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/IndentationRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/IndentationRule.kt @@ -1,68 +1,74 @@ package com.github.shyiko.ktlint.ruleset.standard -import com.github.shyiko.ktlint.core.KtLint import com.github.shyiko.ktlint.core.Rule +import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.com.intellij.lang.ASTNode -import org.jetbrains.kotlin.com.intellij.openapi.util.TextRange +import org.jetbrains.kotlin.com.intellij.lang.FileASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiComment import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace -import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil -import org.jetbrains.kotlin.diagnostics.DiagnosticUtils -import org.jetbrains.kotlin.psi.KtParameter import org.jetbrains.kotlin.psi.KtParameterList -import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType -import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.jetbrains.kotlin.psi.KtTypeConstraintList import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes class IndentationRule : Rule("indent") { - companion object { - // indentation size recommended by JetBrains - private const val DEFAULT_INDENT = 4 - } - - private var indent = DEFAULT_INDENT + private var indentSize = -1 - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node.elementType == KtStubElementTypes.FILE) { - val editorConfig = node.getUserData(KtLint.EDITOR_CONFIG_USER_DATA_KEY)!! - val indentSize = editorConfig.get("indent_size") - indent = indentSize?.toIntOrNull() ?: if (indentSize?.toLowerCase() == "unset") -1 else indent + val ec = EditorConfig.from(node as FileASTNode) + indentSize = gcd(maxOf(ec.indentSize, 1), maxOf(ec.continuationIndentSize, 1)) return } - if (indent <= 0) { + if (indentSize <= 1) { return } - if (node is PsiWhiteSpace && !node.isPartOf(PsiComment::class)) { + if (node is PsiWhiteSpace) { val lines = node.getText().split("\n") - if (lines.size > 1) { + if (lines.size > 1 && !node.isPartOf(PsiComment::class) && !node.isPartOf(KtTypeConstraintList::class)) { var offset = node.startOffset + lines.first().length + 1 - val firstParameterColumn = lazy { - val firstParameter = PsiTreeUtil.findChildOfType( - node.getNonStrictParentOfType(KtParameterList::class.java), - KtParameter::class.java - ) - firstParameter?.run { - DiagnosticUtils.getLineAndColumnInPsiFile(node.containingFile, - TextRange(startOffset, startOffset)).column - } ?: 0 - } - lines.tail().forEach { line -> - if (line.length % indent != 0) { - if (node.isPartOf(KtParameterList::class) && firstParameterColumn.value != 0) { - if (firstParameterColumn.value - 1 != line.length) { - emit(offset, "Unexpected indentation (${line.length}) (" + - "parameters should be either vertically aligned or indented by the multiple of $indent" + - ")", false) - } - } else { - emit(offset, "Unexpected indentation (${line.length}) (it should be multiple of $indent)", false) + val previousIndentSize = node.previousIndentSize() + lines.tail().forEach { indent -> + if (indent.isNotEmpty() && (indent.length - previousIndentSize) % indentSize != 0) { + if (!node.isPartOf(KtParameterList::class)) { // parameter list wrapping enforced by ParameterListWrappingRule + emit( + offset, + "Unexpected indentation (${indent.length}) (it should be ${previousIndentSize + indentSize})", + false + ) } } - offset += line.length + 1 + offset += indent.length + 1 } } } } + + private fun gcd(a: Int, b: Int): Int = when { + a > b -> gcd(a - b, b) + a < b -> gcd(a, b - a) + else -> a + } + + // todo: calculating indent based on the previous line value is wrong (see IndentationRule.testLint) + private fun ASTNode.previousIndentSize(): Int { + var node = this.treeParent?.psi + while (node != null) { + val nextNode = node.nextSibling?.node?.elementType + if (node is PsiWhiteSpace && + nextNode != KtStubElementTypes.TYPE_REFERENCE && + nextNode != KtStubElementTypes.SUPER_TYPE_LIST && + nextNode != KtNodeTypes.CONSTRUCTOR_DELEGATION_CALL && + node.textContains('\n') && + node.nextLeaf()?.isPartOf(PsiComment::class) != true) { + return node.text.length - node.text.lastIndexOf('\n') - 1 + } + node = node.prevSibling ?: node.parent + } + return 0 + } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/MaxLineLengthRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/MaxLineLengthRule.kt index 58d784e0..aa7cca16 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/MaxLineLengthRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/MaxLineLengthRule.kt @@ -1,16 +1,21 @@ package com.github.shyiko.ktlint.ruleset.standard -import com.github.shyiko.ktlint.core.KtLint import com.github.shyiko.ktlint.core.Rule import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.lang.FileASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiComment +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.kdoc.psi.api.KDoc import org.jetbrains.kotlin.psi.KtImportDirective import org.jetbrains.kotlin.psi.KtPackageDirective import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespaceAndComments import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes -class MaxLineLengthRule : Rule("max-line-length") { +class MaxLineLengthRule : Rule("max-line-length"), Rule.Modifier.Last { + + private var maxLineLength: Int = -1 + private var rangeTree = RangeTree() override fun visit( node: ASTNode, @@ -18,31 +23,134 @@ class MaxLineLengthRule : Rule("max-line-length") { emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { if (node.elementType == KtStubElementTypes.FILE) { - val editorConfig = node.getUserData(KtLint.EDITOR_CONFIG_USER_DATA_KEY)!! - val maxLineLength = editorConfig.get("max_line_length")?.toIntOrNull() ?: 0 + val ec = EditorConfig.from(node as FileASTNode) + maxLineLength = ec.maxLineLength if (maxLineLength <= 0) { return } + val errorOffset = arrayListOf<Int>() val text = node.text val lines = text.split("\n") var offset = 0 for (line in lines) { if (line.length > maxLineLength) { val el = node.psi.findElementAt(offset + line.length - 1)!! - if (!el.isPartOf(PsiComment::class)) { - if (!el.isPartOf(KtPackageDirective::class) && !el.isPartOf(KtImportDirective::class)) { - emit(offset, "Exceeded max line length ($maxLineLength)", false) - } - } else { - // if comment is the only thing on the line - fine, otherwise emit an error - val prevLeaf = el.getPrevSiblingIgnoringWhitespaceAndComments(false) - if (prevLeaf != null && prevLeaf.startOffset >= offset) { - emit(offset, "Exceeded max line length ($maxLineLength)", false) + if (!el.isPartOf(KDoc::class)) { + if (!el.isPartOf(PsiComment::class)) { + if (!el.isPartOf(KtPackageDirective::class) && !el.isPartOf(KtImportDirective::class)) { + // fixme: + // normally we would emit here but due to API limitations we need to hold off until + // node spanning the same offset is 'visit'ed + // (for ktlint-disable directive to have effect (when applied)) + // this will be rectified in the upcoming release(s) + errorOffset.add(offset) + } + } else { + // if comment is the only thing on the line - fine, otherwise emit an error + val prevLeaf = el.getPrevSiblingIgnoringWhitespaceAndComments(false) + if (prevLeaf != null && prevLeaf.startOffset >= offset) { + // fixme: + // normally we would emit here but due to API limitations we need to hold off until + // node spanning the same offset is 'visit'ed + // (for ktlint-disable directive to have effect (when applied)) + // this will be rectified in the upcoming release(s) + errorOffset.add(offset) + } } } } offset += line.length + 1 } + rangeTree = RangeTree(errorOffset) + } else if (!rangeTree.isEmpty() && node.psi is LeafPsiElement) { + rangeTree + .query(node.startOffset, node.startOffset + node.textLength) + .forEach { offset -> + emit(offset, "Exceeded max line length ($maxLineLength)", false) + } + } + } +} + +class RangeTree(seq: List<Int> = emptyList()) { + + private var emptyArrayView = ArrayView(0, 0) + private var arr: IntArray = seq.toIntArray() + + init { + if (arr.isNotEmpty()) { + arr.reduce { p, n -> require(p <= n) { "Input must be sorted" }; n } + } + } + + // runtime: O(log(n)+k), where k is number of matching points + // space: O(1) + fun query(vmin: Int, vmax: Int): ArrayView { + var r = arr.size - 1 + if (r == -1 || vmax < arr[0] || arr[r] < vmin) { + return emptyArrayView + } + // binary search for min(arr[l] >= vmin) + var l = 0 + while (l < r) { + val m = (r + l) / 2 + if (vmax < arr[m]) { + r = m - 1 + } else if (arr[m] < vmin) { + l = m + 1 + } else { + // arr[l] ?<=? vmin <= arr[m] <= vmax ?<=? arr[r] + if (vmin <= arr[l]) break else l++ // optimization + r = m + } + } + if (l > r || arr[l] < vmin) { + return emptyArrayView + } + // find max(k) such as arr[k] < vmax + var k = l + while (k < arr.size) { + if (arr[k] >= vmax) { + break + } + k++ + } + return ArrayView(l, k) + } + + fun isEmpty() = arr.isEmpty() + + inner class ArrayView(private var l: Int, private val r: Int) { + + val size: Int = r - l + + fun get(i: Int): Int { + if (i < 0 || i >= size) { + throw IndexOutOfBoundsException() + } + return arr[l + i] + } + + inline fun forEach(cb: (v: Int) -> Unit) { + var i = 0 + while (i < size) { + cb(get(i++)) + } + } + + override fun toString(): String { + if (l == r) { + return "[]" + } + val sb = StringBuilder("[") + var i = l + while (i < r) { + sb.append(arr[i]).append(", ") + i++ + } + sb.replace(sb.length - 2, sb.length, "") + sb.append("]") + return sb.toString() } } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ModifierOrderRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ModifierOrderRule.kt index 4815c065..3c9c9e5d 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ModifierOrderRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ModifierOrderRule.kt @@ -2,14 +2,15 @@ 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.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.lexer.KtTokens.ABSTRACT_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.ACTUAL_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.ANNOTATION_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.COMPANION_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.CONST_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.DATA_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.ENUM_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.EXPECT_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.EXTERNAL_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.FINAL_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.INFIX_KEYWORD @@ -26,25 +27,34 @@ import org.jetbrains.kotlin.lexer.KtTokens.PUBLIC_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.SEALED_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.SUSPEND_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.TAILREC_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.VARARG_KEYWORD +import org.jetbrains.kotlin.psi.KtAnnotationEntry import org.jetbrains.kotlin.psi.KtDeclarationModifierList +import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes.ANNOTATION_ENTRY import java.util.Arrays class ModifierOrderRule : Rule("modifier-order") { - // subset of KtTokens.MODIFIER_KEYWORDS_ARRAY + // subset of KtTokens.MODIFIER_KEYWORDS_ARRAY (+ annotations entries) private val order = arrayOf( + ANNOTATION_ENTRY, PUBLIC_KEYWORD, PROTECTED_KEYWORD, PRIVATE_KEYWORD, INTERNAL_KEYWORD, - FINAL_KEYWORD, OPEN_KEYWORD, ABSTRACT_KEYWORD, - SUSPEND_KEYWORD, TAILREC_KEYWORD, + EXPECT_KEYWORD, ACTUAL_KEYWORD, + FINAL_KEYWORD, OPEN_KEYWORD, ABSTRACT_KEYWORD, SEALED_KEYWORD, CONST_KEYWORD, + EXTERNAL_KEYWORD, OVERRIDE_KEYWORD, - CONST_KEYWORD, LATEINIT_KEYWORD, - INNER_KEYWORD, EXTERNAL_KEYWORD, - ENUM_KEYWORD, ANNOTATION_KEYWORD, SEALED_KEYWORD, DATA_KEYWORD, + LATEINIT_KEYWORD, + TAILREC_KEYWORD, + VARARG_KEYWORD, + SUSPEND_KEYWORD, + INNER_KEYWORD, + ENUM_KEYWORD, ANNOTATION_KEYWORD, COMPANION_KEYWORD, INLINE_KEYWORD, - // NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, OUT_KEYWORD, IN_KEYWORD, VARARG_KEYWORD, REIFIED_KEYWORD INFIX_KEYWORD, - OPERATOR_KEYWORD + OPERATOR_KEYWORD, + DATA_KEYWORD + // NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, OUT_KEYWORD, IN_KEYWORD, REIFIED_KEYWORD // HEADER_KEYWORD, IMPL_KEYWORD ) private val tokenSet = TokenSet.create(*order) @@ -58,16 +68,26 @@ class ModifierOrderRule : Rule("modifier-order") { val modifierArr = node.getChildren(tokenSet) val sorted = modifierArr.copyOf().apply { sortWith(compareBy { order.indexOf(it.elementType) }) } if (!Arrays.equals(modifierArr, sorted)) { + // Since annotations can be fairly lengthy and/or span multiple lines we are + // squashing them into a single placeholder text to guarantee a single line output emit(node.startOffset, "Incorrect modifier order (should be \"${ - sorted.map { it.text }.joinToString(" ") + squashAnnotations(sorted).joinToString(" ") }\")", true) if (autoCorrect) { modifierArr.forEachIndexed { i, n -> - // fixme: find a better way (node type is now potentially out of sync) - (n.psi as LeafPsiElement).replaceWithText(sorted[i].text) + node.replaceChild(n, sorted[i].clone() as ASTNode) } } } } } + + private fun squashAnnotations(sorted: Array<ASTNode>): List<String> { + val nonAnnotationModifiers = sorted.filter { it.psi !is KtAnnotationEntry } + return if (nonAnnotationModifiers.size != sorted.size) { + listOf("@Annotation...") + nonAnnotationModifiers.map { it.text } + } else { + nonAnnotationModifiers.map { it.text } + } + } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoBlankLineBeforeRbraceRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoBlankLineBeforeRbraceRule.kt index ce64489f..cd7649e1 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoBlankLineBeforeRbraceRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoBlankLineBeforeRbraceRule.kt @@ -9,17 +9,20 @@ import org.jetbrains.kotlin.lexer.KtTokens class NoBlankLineBeforeRbraceRule : Rule("no-blank-line-before-rbrace") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node is PsiWhiteSpace && node.textContains('\n') && PsiTreeUtil.nextLeaf(node, true)?.node?.elementType == KtTokens.RBRACE) { val split = node.getText().split("\n") if (split.size > 2) { emit(node.startOffset + split[0].length + split[1].length + 1, - "Needless blank line(s)", true) + "Unexpected blank line(s) before \"}\"", true) if (autoCorrect) { - (node as LeafPsiElement).replaceWithText("${split.first()}\n${split.last()}") + (node as LeafPsiElement).rawReplaceWithText("${split.first()}\n${split.last()}") } } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoConsecutiveBlankLinesRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoConsecutiveBlankLinesRule.kt index 1e6142f8..ae7c582d 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoConsecutiveBlankLinesRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoConsecutiveBlankLinesRule.kt @@ -4,17 +4,22 @@ import com.github.shyiko.ktlint.core.Rule 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.util.PsiTreeUtil class NoConsecutiveBlankLinesRule : Rule("no-consecutive-blank-lines") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node is PsiWhiteSpace) { val split = node.getText().split("\n") - if (split.size > 3) { + if (split.size > 3 || split.size == 3 && PsiTreeUtil.nextLeaf(node) == null /* eof */) { emit(node.startOffset + split[0].length + split[1].length + 2, "Needless blank line(s)", true) if (autoCorrect) { - (node as LeafPsiElement).replaceWithText("${split.first()}\n\n${split.last()}") + (node as LeafPsiElement) + .rawReplaceWithText("${split.first()}\n${if (split.size > 3) "\n" else ""}${split.last()}") } } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoEmptyClassBodyRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoEmptyClassBodyRule.kt index 59a57a34..5e412204 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoEmptyClassBodyRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoEmptyClassBodyRule.kt @@ -25,7 +25,7 @@ class NoEmptyClassBodyRule : Rule("no-empty-class-body") { if (autoCorrect) { val prevNode = node.psi.prevSibling.node val nextNode = PsiTreeUtil.nextLeaf(node.psi, true)?.node - if (prevNode.elementType == KtTokens.WHITE_SPACE && nextNode?.elementType == KtTokens.WHITE_SPACE) { + if (prevNode.elementType == KtTokens.WHITE_SPACE && (nextNode == null || nextNode.elementType == KtTokens.WHITE_SPACE)) { // remove space between declaration and block prevNode.treeParent.removeChild(prevNode) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakAfterElseRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakAfterElseRule.kt new file mode 100644 index 00000000..d28ba5eb --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakAfterElseRule.kt @@ -0,0 +1,28 @@ +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.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.lexer.KtTokens + +class NoLineBreakAfterElseRule : Rule("no-line-break-after-else") { + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node is PsiWhiteSpace && + node.textContains('\n')) { + if (PsiTreeUtil.prevLeaf(node, true)?.node?.elementType == KtTokens.ELSE_KEYWORD && + PsiTreeUtil.nextLeaf(node, true)?.node?.elementType.let { it == KtTokens.IF_KEYWORD || it == KtTokens.LBRACE }) { + emit(node.startOffset + 1, "Unexpected line break after \"else\"", true) + if (autoCorrect) { + (node as LeafPsiElement).rawReplaceWithText(" ") + } + } + } + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakBeforeAssignmentRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakBeforeAssignmentRule.kt new file mode 100644 index 00000000..effbd187 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoLineBreakBeforeAssignmentRule.kt @@ -0,0 +1,23 @@ +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.psi.PsiWhiteSpace +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.lexer.KtTokens + +class NoLineBreakBeforeAssignmentRule : Rule("no-line-break-before-assignment") { + + override fun visit(node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + if (node.elementType == KtTokens.EQ) { + val prevElement = node.treePrev?.psi + if (prevElement is PsiWhiteSpace && prevElement.text.contains("\n")) { + emit(node.startOffset, "Line break before assignment is not allowed", true) + if (autoCorrect) { + (node.treeNext?.psi as LeafPsiElement).rawReplaceWithText(prevElement.text) + (prevElement as LeafPsiElement).rawReplaceWithText(" ") + } + } + } + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoMultipleSpacesRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoMultipleSpacesRule.kt index 36cf26fe..15aa1a2a 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoMultipleSpacesRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoMultipleSpacesRule.kt @@ -33,7 +33,8 @@ class NoMultipleSpacesRule : Rule("no-multi-spaces") { val psi = node.psi if (psi is PsiComment) { comments.add(psi) } } - return comments.foldIndexed(mutableMapOf<Offset, CommentRelativeLocation>()) { i, acc, comment -> + return comments.foldIndexed(mutableMapOf()) { i, acc, comment -> + // todo: get rid of DiagnosticUtils (IndexOutOfBoundsException) val pos = DiagnosticUtils.getLineAndColumnInPsiFile(fileNode.psi as PsiFile, TextRange(comment.startOffset, comment.startOffset)) acc.put(comment.startOffset, CommentRelativeLocation( @@ -46,12 +47,14 @@ class NoMultipleSpacesRule : Rule("no-multi-spaces") { } } - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + 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) { + } else if (node is PsiWhiteSpace && !node.textContains('\n') && node.getTextLength() > 1) { val nextLeaf = PsiTreeUtil.nextLeaf(node, true) if (nextLeaf is PsiComment) { val positionMap = commentMap @@ -71,13 +74,8 @@ class NoMultipleSpacesRule : Rule("no-multi-spaces") { } emit(node.startOffset + 1, "Unnecessary space(s)", true) if (autoCorrect) { - (node as LeafPsiElement).replaceWithText(" ") + (node as LeafPsiElement).rawReplaceWithText(" ") } } } - - private fun ASTNode.visit(cb: (node: ASTNode) -> Unit) { - cb(this) - this.getChildren(null).forEach { it.visit(cb) } - } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoSemicolonsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoSemicolonsRule.kt index fce5ce3c..770e4090 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoSemicolonsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoSemicolonsRule.kt @@ -10,8 +10,11 @@ import org.jetbrains.kotlin.psi.KtEnumEntry class NoSemicolonsRule : Rule("no-semi") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node is LeafPsiElement && node.textMatches(";") && !node.isPartOfString() && !node.isPartOf(KtEnumEntry::class)) { val nextLeaf = PsiTreeUtil.nextLeaf(node, true) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoTrailingSpacesRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoTrailingSpacesRule.kt index fa3e492f..a7e03c88 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoTrailingSpacesRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoTrailingSpacesRule.kt @@ -8,33 +8,41 @@ import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil class NoTrailingSpacesRule : Rule("no-trailing-spaces") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node is PsiWhiteSpace) { val lines = node.getText().split("\n") if (lines.size > 1) { - checkForTrailingSpaces(lines.head(), node.startOffset, emit) - if (autoCorrect) { - (node as LeafPsiElement).replaceWithText("\n".repeat(lines.size - 1) + lines.last()) + val violated = checkForTrailingSpaces(lines.head(), node.startOffset, emit) + if (violated && autoCorrect) { + (node as LeafPsiElement).rawReplaceWithText("\n".repeat(lines.size - 1) + lines.last()) } - } else - if (PsiTreeUtil.nextLeaf(node) == null /* eof */) { - checkForTrailingSpaces(lines, node.startOffset, emit) - if (autoCorrect) { - (node as LeafPsiElement).replaceWithText("\n".repeat(lines.size - 1)) + } else if (PsiTreeUtil.nextLeaf(node) == null /* eof */) { + val violated = checkForTrailingSpaces(lines, node.startOffset, emit) + if (violated && autoCorrect) { + (node as LeafPsiElement).rawReplaceWithText("\n".repeat(lines.size - 1)) } } } } - private fun checkForTrailingSpaces(lines: List<String>, offset: Int, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + private fun checkForTrailingSpaces( + lines: List<String>, + offset: Int, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ): Boolean { + var violated = false var violationOffset = offset - return lines.forEach { line -> + lines.forEach { line -> if (!line.isEmpty()) { emit(violationOffset, "Trailing space(s)", true) + violated = true } violationOffset += line.length + 1 } + return violated } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnitReturnRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnitReturnRule.kt index 205448a3..c911cbbd 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnitReturnRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnitReturnRule.kt @@ -13,10 +13,10 @@ class NoUnitReturnRule : Rule("no-unit-return") { autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { - if (node.elementType == KtStubElementTypes.TYPE_REFERENCE - && node.treeParent.elementType == KtStubElementTypes.FUNCTION - && node.text.contentEquals("Unit") - && PsiTreeUtil.nextVisibleLeaf(node.psi)?.node?.elementType == KtTokens.LBRACE) { + if (node.elementType == KtStubElementTypes.TYPE_REFERENCE && + node.treeParent.elementType == KtStubElementTypes.FUNCTION && + node.text.contentEquals("Unit") && + PsiTreeUtil.nextVisibleLeaf(node.psi)?.node?.elementType == KtTokens.LBRACE) { emit(node.startOffset, "Unnecessary \"Unit\" return type", true) if (autoCorrect) { var prevNode = node diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRule.kt index cbb8287d..b46f04df 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRule.kt @@ -12,6 +12,8 @@ import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes class NoUnusedImportsRule : Rule("no-unused-imports") { + private val componentNRegex = Regex("^component\\d+$") + private val operatorSet = setOf( // unary "unaryPlus", "unaryMinus", "not", @@ -34,46 +36,45 @@ class NoUnusedImportsRule : Rule("no-unused-imports") { // iteration (https://github.com/shyiko/ktlint/issues/40) "iterator", // by (https://github.com/shyiko/ktlint/issues/54) - "getValue", "setValue", - // destructuring assignment - "component1", "component2", "component3", "component4", "component5" + "getValue", "setValue" ) - private val ref = mutableSetOf("*") + private val ref = mutableSetOf<String>() private var packageName = "" - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node.elementType == KtStubElementTypes.FILE) { + ref.clear() // rule can potentially be executed more than once (when formatting) + ref.add("*") node.visit { vnode -> val psi = vnode.psi val type = vnode.elementType if (type == KDocTokens.MARKDOWN_LINK && psi is KDocLink) { val linkText = psi.getLinkText().replace("`", "") ref.add(linkText.split('.').first()) - } else - if ((type == KtNodeTypes.REFERENCE_EXPRESSION || type == KtNodeTypes.OPERATION_REFERENCE) && + } else if ((type == KtNodeTypes.REFERENCE_EXPRESSION || type == KtNodeTypes.OPERATION_REFERENCE) && !psi.isPartOf(KtImportDirective::class)) { ref.add(vnode.text.trim('`')) } } - } else - if (node.elementType == KtStubElementTypes.PACKAGE_DIRECTIVE) { + } else if (node.elementType == KtStubElementTypes.PACKAGE_DIRECTIVE) { val packageDirective = node.psi as KtPackageDirective packageName = packageDirective.qualifiedName - } else - if (node.elementType == KtStubElementTypes.IMPORT_DIRECTIVE) { + } else if (node.elementType == KtStubElementTypes.IMPORT_DIRECTIVE) { val importDirective = node.psi as KtImportDirective val name = importDirective.importPath?.importedName?.asString() val importPath = importDirective.importPath?.pathStr!! if (importDirective.aliasName == null && - importPath.startsWith(packageName) && + (packageName.isEmpty() || importPath.startsWith("$packageName.")) && importPath.substring(packageName.length + 1).indexOf('.') == -1) { emit(importDirective.startOffset, "Unnecessary import", true) if (autoCorrect) { importDirective.delete() } - } else - if (name != null && !ref.contains(name) && !operatorSet.contains(name)) { + } else if (name != null && !ref.contains(name) && !operatorSet.contains(name) && !name.isComponentN()) { emit(importDirective.startOffset, "Unused import", true) if (autoCorrect) { importDirective.delete() @@ -82,8 +83,5 @@ class NoUnusedImportsRule : Rule("no-unused-imports") { } } - private fun ASTNode.visit(cb: (node: ASTNode) -> Unit) { - cb(this) - this.getChildren(null).forEach { it.visit(cb) } - } + private fun String.isComponentN() = componentNRegex.matches(this) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoWildcardImportsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoWildcardImportsRule.kt index db4e7bb9..9cb18aa1 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoWildcardImportsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoWildcardImportsRule.kt @@ -7,8 +7,11 @@ import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes class NoWildcardImportsRule : Rule("no-wildcard-imports") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node.elementType == KtStubElementTypes.IMPORT_DIRECTIVE) { val importDirective = node.psi as KtImportDirective val path = importDirective.importPath?.pathStr diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ParameterListWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ParameterListWrappingRule.kt new file mode 100644 index 00000000..81d06038 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ParameterListWrappingRule.kt @@ -0,0 +1,151 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.Rule +import org.jetbrains.kotlin.KtNodeTypes +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.lang.FileASTNode +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement +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.com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.psiUtil.children +import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes + +class ParameterListWrappingRule : Rule("parameter-list-wrapping") { + + private var indentSize = -1 + private var maxLineLength = -1 + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.elementType == KtStubElementTypes.FILE) { + val ec = EditorConfig.from(node as FileASTNode) + indentSize = ec.indentSize + maxLineLength = ec.maxLineLength + return + } + if (indentSize <= 0) { + return + } + if (node.elementType == KtStubElementTypes.VALUE_PARAMETER_LIST && + // skip lambda parameters + node.treeParent?.elementType != KtNodeTypes.FUNCTION_LITERAL) { + // each parameter should be on a separate line if + // - at least one of the parameters is + // - maxLineLength exceeded (and separating parameters with \n would actually help) + // in addition, "(" and ")" must be on separates line if any of the parameters are (otherwise on the same) + val putParametersOnSeparateLines = node.textContains('\n') || + // max_line_length exceeded + maxLineLength > -1 && (node.psi.column - 1 + node.textLength) > maxLineLength + if (putParametersOnSeparateLines) { + // aiming for + // ... LPAR + // <line indent + indentSize> VALUE_PARAMETER... + // <line indent> RPAR + val indent = "\n" + node.psi.lineIndent() + val paramIndent = indent + " ".repeat(indentSize) // single indent as recommended by Jetbrains/Google + nextChild@ for (child in node.children()) { + when (child.elementType) { + KtTokens.LPAR -> { + val prevLeaf = child.psi.prevLeaf() + if (prevLeaf is PsiWhiteSpace && prevLeaf.textContains('\n')) { + emit(child.startOffset, errorMessage(child), true) + if (autoCorrect) { + prevLeaf.delete() + } + } + } + KtStubElementTypes.VALUE_PARAMETER, + KtTokens.RPAR -> { + var paramInnerIndentAdjustment = 0 + val prevLeaf = child.psi.prevLeaf() + val intendedIndent = if (child.elementType == KtStubElementTypes.VALUE_PARAMETER) + paramIndent else indent + if (prevLeaf is PsiWhiteSpace) { + val spacing = prevLeaf.text + val cut = spacing.lastIndexOf("\n") + if (cut > -1) { + val childIndent = spacing.substring(cut) + if (childIndent == intendedIndent) { + continue@nextChild + } + emit(child.startOffset, "Unexpected indentation" + + " (expected ${intendedIndent.length - 1}, actual ${childIndent.length - 1})", true) + } else { + emit(child.startOffset, errorMessage(child), true) + } + if (autoCorrect) { + val adjustedIndent = (if (cut > -1) spacing.substring(0, cut) else "") + intendedIndent + paramInnerIndentAdjustment = adjustedIndent.length - prevLeaf.textLength + (prevLeaf as LeafPsiElement).rawReplaceWithText(adjustedIndent) + } + } else { + emit(child.startOffset, errorMessage(child), true) + if (autoCorrect) { + paramInnerIndentAdjustment = intendedIndent.length - child.psi.column + node.addChild(PsiWhiteSpaceImpl(intendedIndent), child) + } + } + if (paramInnerIndentAdjustment != 0 && + child.elementType == KtStubElementTypes.VALUE_PARAMETER) { + child.visit { n -> + if (n.elementType == KtTokens.WHITE_SPACE && n.textContains('\n')) { + val split = n.text.split("\n") + (n.psi as LeafElement).rawReplaceWithText(split.joinToString("\n") { + if (paramInnerIndentAdjustment > 0) { + it + " ".repeat(paramInnerIndentAdjustment) + } else { + it.substring(0, Math.max(it.length + paramInnerIndentAdjustment, 0)) + } + }) + } + } + } + } + } + } + } + } + } + + private val PsiElement.column: Int + get() { + var leaf = PsiTreeUtil.prevLeaf(this) + var offsetToTheLeft = 0 + while (leaf != null) { + if (leaf.node.elementType == KtTokens.WHITE_SPACE && leaf.textContains('\n')) { + offsetToTheLeft += leaf.textLength - 1 - leaf.text.lastIndexOf('\n') + break + } + offsetToTheLeft += leaf.textLength + leaf = PsiTreeUtil.prevLeaf(leaf) + } + return offsetToTheLeft + 1 + } + + private fun errorMessage(node: ASTNode) = + when (node.elementType) { + KtTokens.LPAR -> """Unnecessary newline before "("""" + KtStubElementTypes.VALUE_PARAMETER -> + "Parameter should be on a separate line (unless all parameters can fit a single line)" + KtTokens.RPAR -> """Missing newline before ")"""" + else -> throw UnsupportedOperationException() + } + + private fun PsiElement.lineIndent(): String { + var leaf = PsiTreeUtil.prevLeaf(this) + while (leaf != null) { + if (leaf.node.elementType == KtTokens.WHITE_SPACE && leaf.textContains('\n')) { + return leaf.text.substring(leaf.text.lastIndexOf('\n') + 1) + } + leaf = PsiTreeUtil.prevLeaf(leaf) + } + return "" + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundColonRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundColonRule.kt index bb9aca83..e2d6b6a7 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundColonRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundColonRule.kt @@ -14,8 +14,11 @@ import org.jetbrains.kotlin.psi.KtTypeParameterList class SpacingAroundColonRule : Rule("colon-spacing") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node is LeafPsiElement && node.textMatches(":") && !node.isPartOfString()) { if (node.isPartOf(KtAnnotation::class) || node.isPartOf(KtAnnotationEntry::class)) { // todo: enfore "no spacing" diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCommaRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCommaRule.kt index 2ebca751..2be2935a 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCommaRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCommaRule.kt @@ -6,16 +6,28 @@ 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.com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.psi.psiUtil.startOffset class SpacingAroundCommaRule : Rule("comma-spacing") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { - if (node is LeafPsiElement && node.textMatches(",") && !node.isPartOfString() && - PsiTreeUtil.nextLeaf(node) !is PsiWhiteSpace) { - emit(node.startOffset + 1, "Missing spacing after \"${node.text}\"", true) - if (autoCorrect) { - node.rawInsertAfterMe(PsiWhiteSpaceImpl(" ")) + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node is LeafPsiElement && node.textMatches(",") && !node.isPartOfString()) { + val prevLeaf = PsiTreeUtil.prevLeaf(node, true) + if (prevLeaf is PsiWhiteSpace) { + emit(prevLeaf.startOffset, "Unexpected spacing before \"${node.text}\"", true) + if (autoCorrect) { + prevLeaf.node.treeParent.removeChild(prevLeaf.node) + } + } + if (PsiTreeUtil.nextLeaf(node) !is PsiWhiteSpace) { + emit(node.startOffset + 1, "Missing spacing after \"${node.text}\"", true) + if (autoCorrect) { + node.rawInsertAfterMe(PsiWhiteSpaceImpl(" ")) + } } } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCurlyRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCurlyRule.kt index 2f6d226b..2bce695e 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCurlyRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundCurlyRule.kt @@ -13,20 +13,25 @@ import org.jetbrains.kotlin.psi.KtLambdaExpression class SpacingAroundCurlyRule : Rule("curly-spacing") { - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node is LeafPsiElement && !node.isPartOfString()) { val prevLeaf = PsiTreeUtil.prevLeaf(node, true) val nextLeaf = PsiTreeUtil.nextLeaf(node, true) val spacingBefore: Boolean val spacingAfter: Boolean if (node.textMatches("{")) { - spacingBefore = prevLeaf is PsiWhiteSpace || (prevLeaf?.node?.elementType == KtTokens.LPAR && + spacingBefore = prevLeaf is PsiWhiteSpace || prevLeaf?.node?.elementType == KtTokens.AT || (prevLeaf?.node?.elementType == KtTokens.LPAR && (node.parent is KtLambdaExpression || node.parent.parent is KtLambdaExpression)) spacingAfter = nextLeaf is PsiWhiteSpace || nextLeaf?.node?.elementType == KtTokens.RBRACE if (prevLeaf is PsiWhiteSpace && - !prevLeaf.textContains('\n') && - PsiTreeUtil.prevLeaf(prevLeaf, true)?.node?.elementType == KtTokens.LPAR) { + !prevLeaf.textContains('\n') && + PsiTreeUtil.prevLeaf(prevLeaf, true)?.node?.let { + it.elementType == KtTokens.LPAR || it.elementType == KtTokens.AT + } == true) { emit(node.startOffset, "Unexpected space before \"${node.text}\"", true) if (autoCorrect) { prevLeaf.node.treeParent.removeChild(prevLeaf.node) @@ -41,11 +46,10 @@ class SpacingAroundCurlyRule : Rule("curly-spacing") { node.parent.node.elementType == KtNodeTypes.CLASS_BODY)) { emit(node.startOffset, "Unexpected newline before \"${node.text}\"", true) if (autoCorrect) { - (prevLeaf.node as LeafPsiElement).replaceWithText(" ") + (prevLeaf.node as LeafPsiElement).rawReplaceWithText(" ") } } - } else - if (node.textMatches("}")) { + } else if (node.textMatches("}")) { spacingBefore = prevLeaf is PsiWhiteSpace || prevLeaf?.node?.elementType == KtTokens.LBRACE spacingAfter = nextLeaf == null || nextLeaf is PsiWhiteSpace || shouldNotToBeSeparatedBySpace(nextLeaf) if (nextLeaf is PsiWhiteSpace && !nextLeaf.textContains('\n') && diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundKeywordRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundKeywordRule.kt index 15bb5597..ec31bf9b 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundKeywordRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundKeywordRule.kt @@ -29,8 +29,11 @@ class SpacingAroundKeywordRule : Rule("keyword-spacing") { private val keywordsWithoutSpaces = TokenSet.create(KtTokens.GET_KEYWORD, KtTokens.SET_KEYWORD) - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node is LeafPsiElement) { if (tokenSet.contains(node.elementType) && node.nextLeaf() !is PsiWhiteSpace) { diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundOperatorsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundOperatorsRule.kt index 4bc9448a..06dd72ee 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundOperatorsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundOperatorsRule.kt @@ -36,24 +36,36 @@ import org.jetbrains.kotlin.psi.KtSuperExpression import org.jetbrains.kotlin.psi.KtTypeArgumentList import org.jetbrains.kotlin.psi.KtTypeParameterList import org.jetbrains.kotlin.psi.KtValueArgument +import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType +import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes class SpacingAroundOperatorsRule : Rule("op-spacing") { private val tokenSet = TokenSet.create(MUL, PLUS, MINUS, DIV, PERC, LT, GT, LTEQ, GTEQ, EQEQEQ, EXCLEQEQEQ, EQEQ, EXCLEQ, ANDAND, OROR, ELVIS, EQ, MULTEQ, DIVEQ, PERCEQ, PLUSEQ, MINUSEQ, ARROW) - override fun visit(node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (tokenSet.contains(node.elementType) && node is LeafPsiElement && !node.isPartOf(KtPrefixExpression::class) && // not unary - !node.isPartOf(KtTypeParameterList::class) && // fun <T>fn(): T {} !node.isPartOf(KtTypeArgumentList::class) && // C<T> - !node.isPartOf(KtValueArgument::class) && // fn(*array) + !(node.elementType == MUL && node.isPartOf(KtValueArgument::class)) && // fn(*array) !node.isPartOf(KtImportDirective::class) && // import * !node.isPartOf(KtSuperExpression::class) // super<T> ) { - val spacingBefore = PsiTreeUtil.prevLeaf(node, true) is PsiWhiteSpace - val spacingAfter = PsiTreeUtil.nextLeaf(node, true) is PsiWhiteSpace + if ((node.elementType == GT || node.elementType == LT) && + // fun <T>fn(): T {} + node.getNonStrictParentOfType(KtTypeParameterList::class.java)?.parent?.node?.elementType != + KtStubElementTypes.FUNCTION) { + return + } + val spacingBefore = PsiTreeUtil.prevLeaf(node, true) is PsiWhiteSpace || + node.elementType == GT + val spacingAfter = PsiTreeUtil.nextLeaf(node, true) is PsiWhiteSpace || + node.elementType == LT when { !spacingBefore && !spacingAfter -> { emit(node.startOffset, "Missing spacing around \"${node.text}\"", true) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundRangeOperatorRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundRangeOperatorRule.kt new file mode 100644 index 00000000..d7bedbea --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/SpacingAroundRangeOperatorRule.kt @@ -0,0 +1,42 @@ +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.psi.PsiWhiteSpace +import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.lexer.KtTokens + +class SpacingAroundRangeOperatorRule : Rule("range-spacing") { + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.elementType == KtTokens.RANGE) { + val prevLeaf = PsiTreeUtil.prevLeaf(node.psi, true) + val nextLeaf = PsiTreeUtil.nextLeaf(node.psi, true) + when { + prevLeaf is PsiWhiteSpace && nextLeaf is PsiWhiteSpace -> { + emit(node.startOffset, "Unexpected spacing around \"..\"", true) + if (autoCorrect) { + prevLeaf.node.treeParent.removeChild(prevLeaf.node) + nextLeaf.node.treeParent.removeChild(nextLeaf.node) + } + } + prevLeaf is PsiWhiteSpace -> { + emit(prevLeaf.node.startOffset, "Unexpected spacing before \"..\"", true) + if (autoCorrect) { + prevLeaf.node.treeParent.removeChild(prevLeaf.node) + } + } + nextLeaf is PsiWhiteSpace -> { + emit(nextLeaf.node.startOffset, "Unexpected spacing after \"..\"", true) + if (autoCorrect) { + nextLeaf.node.treeParent.removeChild(nextLeaf.node) + } + } + } + } + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StandardRuleSetProvider.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StandardRuleSetProvider.kt index 9fd9190c..87f06b42 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StandardRuleSetProvider.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StandardRuleSetProvider.kt @@ -6,6 +6,9 @@ import com.github.shyiko.ktlint.core.RuleSetProvider class StandardRuleSetProvider : RuleSetProvider { override fun get(): RuleSet = RuleSet("standard", + ChainWrappingRule(), + CommentSpacingRule(), + FilenameRule(), FinalNewlineRule(), // disabled until it's clear how to reconcile difference in Intellij & Android Studio import layout // ImportOrderingRule(), @@ -17,17 +20,21 @@ class StandardRuleSetProvider : RuleSetProvider { NoEmptyClassBodyRule(), // disabled until it's clear what to do in case of `import _.it` // NoItParamInMultilineLambdaRule(), + NoLineBreakAfterElseRule(), + NoLineBreakBeforeAssignmentRule(), NoMultipleSpacesRule(), NoSemicolonsRule(), NoTrailingSpacesRule(), NoUnitReturnRule(), NoUnusedImportsRule(), NoWildcardImportsRule(), + ParameterListWrappingRule(), SpacingAroundColonRule(), SpacingAroundCommaRule(), SpacingAroundCurlyRule(), SpacingAroundKeywordRule(), SpacingAroundOperatorsRule(), + SpacingAroundRangeOperatorRule(), StringTemplateRule() ) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StringTemplateRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StringTemplateRule.kt index f58a4bfe..e63d7927 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StringTemplateRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StringTemplateRule.kt @@ -32,26 +32,27 @@ class StringTemplateRule : Rule("string-template") { if (dotQualifiedExpression?.node?.elementType == KtStubElementTypes.DOT_QUALIFIED_EXPRESSION) { val callExpression = dotQualifiedExpression!!.lastChild val dot = callExpression.prevSibling - if (dot.node.elementType == KtTokens.DOT && callExpression.text == "toString()" && - dotQualifiedExpression.firstChild.node.elementType != KtNodeTypes.SUPER_EXPRESSION) { - emit(dot.node.startOffset, "Redundant 'toString()' call in string template", true) + if (dot?.node?.elementType == KtTokens.DOT && + callExpression.text == "toString()" && + dotQualifiedExpression.firstChild?.node?.elementType != KtNodeTypes.SUPER_EXPRESSION) { + emit(dot.node.startOffset, "Redundant \"toString()\" call in string template", true) if (autoCorrect) { node.removeChild(dot.node) node.removeChild(callExpression.node) } } } - } - if (elementType == KtNodeTypes.LONG_STRING_TEMPLATE_ENTRY && - node.text.let { it.substring(2, it.length - 1) }.all { it.isPartOfIdentifier() } && - (node.treeNext.elementType == KtTokens.CLOSING_QUOTE || - (node.psi.nextSibling.node.elementType == KtNodeTypes.LITERAL_STRING_TEMPLATE_ENTRY && - !node.psi.nextSibling.text[0].isPartOfIdentifier()))) { - emit(node.treePrev.startOffset + 2, "Redundant curly braces", true) - if (autoCorrect) { - // fixme: a proper way would be to downcast to SHORT_STRING_TEMPLATE_ENTRY - (node.psi.firstChild as LeafPsiElement).rawReplaceWithText("$") // entry start - (node.psi.lastChild as LeafPsiElement).rawReplaceWithText("") // entry end + if (node.text.startsWith("${'$'}{") && + node.text.let { it.substring(2, it.length - 1) }.all { it.isPartOfIdentifier() } && + (node.treeNext.elementType == KtTokens.CLOSING_QUOTE || + (node.psi.nextSibling.node.elementType == KtNodeTypes.LITERAL_STRING_TEMPLATE_ENTRY && + !node.psi.nextSibling.text[0].isPartOfIdentifier()))) { + emit(node.treePrev.startOffset + 2, "Redundant curly braces", true) + if (autoCorrect) { + // fixme: a proper way would be to downcast to SHORT_STRING_TEMPLATE_ENTRY + (node.psi.firstChild as LeafPsiElement).rawReplaceWithText("$") // entry start + (node.psi.lastChild as LeafPsiElement).rawReplaceWithText("") // entry end + } } } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/package.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/package.kt index 2acc5c2d..d9e03b1e 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/package.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/package.kt @@ -1,12 +1,20 @@ package com.github.shyiko.ktlint.ruleset.standard +import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil import org.jetbrains.kotlin.psi.KtStringTemplateEntry import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType import kotlin.reflect.KClass internal fun PsiElement.isPartOf(clazz: KClass<out PsiElement>) = getNonStrictParentOfType(clazz.java) != null internal fun PsiElement.isPartOfString() = isPartOf(KtStringTemplateEntry::class) +internal fun PsiElement.prevLeaf(): PsiElement? = PsiTreeUtil.prevLeaf(this) +internal fun PsiElement.nextLeaf(): PsiElement? = PsiTreeUtil.nextLeaf(this) +internal fun ASTNode.visit(cb: (node: ASTNode) -> Unit) { + cb(this) + this.getChildren(null).forEach { it.visit(cb) } +} -internal fun <T>List<T>.head() = this.subList(0, this.size - 1) -internal fun <T>List<T>.tail() = this.subList(1, this.size) +internal fun <T> List<T>.head() = this.subList(0, this.size - 1) +internal fun <T> List<T>.tail() = this.subList(1, this.size) |