com.pinterest.ktlint.ruleset.standard.rules.IndentationRule.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ktlint-ruleset-standard Show documentation
Show all versions of ktlint-ruleset-standard Show documentation
An anti-bikeshedding Kotlin linter with built-in formatter.
package com.pinterest.ktlint.ruleset.standard.rules
import com.pinterest.ktlint.logger.api.initKtLintKLogger
import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATED_EXPRESSION
import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATION
import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATION_ENTRY
import com.pinterest.ktlint.rule.engine.core.api.ElementType.ARRAY_ACCESS_EXPRESSION
import com.pinterest.ktlint.rule.engine.core.api.ElementType.ARROW
import com.pinterest.ktlint.rule.engine.core.api.ElementType.BINARY_EXPRESSION
import com.pinterest.ktlint.rule.engine.core.api.ElementType.BINARY_WITH_TYPE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.BLOCK
import com.pinterest.ktlint.rule.engine.core.api.ElementType.BLOCK_COMMENT
import com.pinterest.ktlint.rule.engine.core.api.ElementType.BODY
import com.pinterest.ktlint.rule.engine.core.api.ElementType.CALL_EXPRESSION
import com.pinterest.ktlint.rule.engine.core.api.ElementType.CATCH
import com.pinterest.ktlint.rule.engine.core.api.ElementType.CLASS
import com.pinterest.ktlint.rule.engine.core.api.ElementType.CLASS_BODY
import com.pinterest.ktlint.rule.engine.core.api.ElementType.CLOSING_QUOTE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.COLON
import com.pinterest.ktlint.rule.engine.core.api.ElementType.CONDITION
import com.pinterest.ktlint.rule.engine.core.api.ElementType.CONSTRUCTOR_DELEGATION_CALL
import com.pinterest.ktlint.rule.engine.core.api.ElementType.CONTEXT_RECEIVER_LIST
import com.pinterest.ktlint.rule.engine.core.api.ElementType.DELEGATED_SUPER_TYPE_ENTRY
import com.pinterest.ktlint.rule.engine.core.api.ElementType.DESTRUCTURING_DECLARATION
import com.pinterest.ktlint.rule.engine.core.api.ElementType.DOT
import com.pinterest.ktlint.rule.engine.core.api.ElementType.DOT_QUALIFIED_EXPRESSION
import com.pinterest.ktlint.rule.engine.core.api.ElementType.ELSE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.ELVIS
import com.pinterest.ktlint.rule.engine.core.api.ElementType.EQ
import com.pinterest.ktlint.rule.engine.core.api.ElementType.FINALLY
import com.pinterest.ktlint.rule.engine.core.api.ElementType.FOR
import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUN
import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUNCTION_LITERAL
import com.pinterest.ktlint.rule.engine.core.api.ElementType.IDENTIFIER
import com.pinterest.ktlint.rule.engine.core.api.ElementType.IF
import com.pinterest.ktlint.rule.engine.core.api.ElementType.KDOC
import com.pinterest.ktlint.rule.engine.core.api.ElementType.KDOC_END
import com.pinterest.ktlint.rule.engine.core.api.ElementType.KDOC_LEADING_ASTERISK
import com.pinterest.ktlint.rule.engine.core.api.ElementType.KDOC_START
import com.pinterest.ktlint.rule.engine.core.api.ElementType.LBRACE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.LBRACKET
import com.pinterest.ktlint.rule.engine.core.api.ElementType.LITERAL_STRING_TEMPLATE_ENTRY
import com.pinterest.ktlint.rule.engine.core.api.ElementType.LONG_STRING_TEMPLATE_ENTRY
import com.pinterest.ktlint.rule.engine.core.api.ElementType.LPAR
import com.pinterest.ktlint.rule.engine.core.api.ElementType.MODIFIER_LIST
import com.pinterest.ktlint.rule.engine.core.api.ElementType.NULLABLE_TYPE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.OBJECT_DECLARATION
import com.pinterest.ktlint.rule.engine.core.api.ElementType.OPEN_QUOTE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.OPERATION_REFERENCE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.PARENTHESIZED
import com.pinterest.ktlint.rule.engine.core.api.ElementType.PRIMARY_CONSTRUCTOR
import com.pinterest.ktlint.rule.engine.core.api.ElementType.PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.ElementType.PROPERTY_ACCESSOR
import com.pinterest.ktlint.rule.engine.core.api.ElementType.RBRACE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.RBRACKET
import com.pinterest.ktlint.rule.engine.core.api.ElementType.REFERENCE_EXPRESSION
import com.pinterest.ktlint.rule.engine.core.api.ElementType.REGULAR_STRING_PART
import com.pinterest.ktlint.rule.engine.core.api.ElementType.RETURN_KEYWORD
import com.pinterest.ktlint.rule.engine.core.api.ElementType.RPAR
import com.pinterest.ktlint.rule.engine.core.api.ElementType.SAFE_ACCESS_EXPRESSION
import com.pinterest.ktlint.rule.engine.core.api.ElementType.SECONDARY_CONSTRUCTOR
import com.pinterest.ktlint.rule.engine.core.api.ElementType.SHORT_STRING_TEMPLATE_ENTRY
import com.pinterest.ktlint.rule.engine.core.api.ElementType.STRING_TEMPLATE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.SUPER_TYPE_CALL_ENTRY
import com.pinterest.ktlint.rule.engine.core.api.ElementType.SUPER_TYPE_ENTRY
import com.pinterest.ktlint.rule.engine.core.api.ElementType.SUPER_TYPE_LIST
import com.pinterest.ktlint.rule.engine.core.api.ElementType.THEN
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TRY
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPEALIAS
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_ARGUMENT_LIST
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_CONSTRAINT
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_CONSTRAINT_LIST
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_PARAMETER_LIST
import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_REFERENCE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.USER_TYPE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT_LIST
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER_LIST
import com.pinterest.ktlint.rule.engine.core.api.ElementType.WHEN
import com.pinterest.ktlint.rule.engine.core.api.ElementType.WHEN_ENTRY
import com.pinterest.ktlint.rule.engine.core.api.ElementType.WHERE_KEYWORD
import com.pinterest.ktlint.rule.engine.core.api.ElementType.WHILE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.WHITE_SPACE
import com.pinterest.ktlint.rule.engine.core.api.IndentConfig
import com.pinterest.ktlint.rule.engine.core.api.IndentConfig.IndentStyle.SPACE
import com.pinterest.ktlint.rule.engine.core.api.IndentConfig.IndentStyle.TAB
import com.pinterest.ktlint.rule.engine.core.api.Rule.VisitorModifier.RunAfterRule.Mode.REGARDLESS_WHETHER_RUN_AFTER_RULE_IS_LOADED_OR_DISABLED
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import com.pinterest.ktlint.rule.engine.core.api.children
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue.ktlint_official
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.firstChildLeafOrSelf
import com.pinterest.ktlint.rule.engine.core.api.indent
import com.pinterest.ktlint.rule.engine.core.api.isPartOfComment
import com.pinterest.ktlint.rule.engine.core.api.isRoot
import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace
import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline
import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithoutNewline
import com.pinterest.ktlint.rule.engine.core.api.lastChildLeafOrSelf
import com.pinterest.ktlint.rule.engine.core.api.nextCodeLeaf
import com.pinterest.ktlint.rule.engine.core.api.nextCodeSibling
import com.pinterest.ktlint.rule.engine.core.api.nextLeaf
import com.pinterest.ktlint.rule.engine.core.api.nextSibling
import com.pinterest.ktlint.rule.engine.core.api.parent
import com.pinterest.ktlint.rule.engine.core.api.prevCodeLeaf
import com.pinterest.ktlint.rule.engine.core.api.prevCodeSibling
import com.pinterest.ktlint.rule.engine.core.api.prevLeaf
import com.pinterest.ktlint.rule.engine.core.api.prevSibling
import com.pinterest.ktlint.ruleset.standard.StandardRule
import mu.KotlinLogging
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.PsiComment
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
import org.jetbrains.kotlin.psi.psiUtil.leaves
import org.jetbrains.kotlin.psi.psiUtil.parents
import java.util.Deque
import java.util.LinkedList
private val LOGGER = KotlinLogging.logger {}.initKtLintKLogger()
public class IndentationRule :
StandardRule(
id = "indent",
visitorModifiers =
setOf(
VisitorModifier.RunAsLateAsPossible,
VisitorModifier.RunAfterRule(
ruleId = FUNCTION_SIGNATURE_RULE_ID,
mode = REGARDLESS_WHETHER_RUN_AFTER_RULE_IS_LOADED_OR_DISABLED,
),
VisitorModifier.RunAfterRule(
ruleId = TRAILING_COMMA_ON_CALL_SITE_RULE_ID,
mode = REGARDLESS_WHETHER_RUN_AFTER_RULE_IS_LOADED_OR_DISABLED,
),
VisitorModifier.RunAfterRule(
ruleId = TRAILING_COMMA_ON_DECLARATION_SITE_RULE_ID,
mode = REGARDLESS_WHETHER_RUN_AFTER_RULE_IS_LOADED_OR_DISABLED,
),
),
usesEditorConfigProperties =
setOf(
CODE_STYLE_PROPERTY,
INDENT_SIZE_PROPERTY,
INDENT_STYLE_PROPERTY,
),
) {
private var codeStyle = CODE_STYLE_PROPERTY.defaultValue
private var indentConfig = IndentConfig.DEFAULT_INDENT_CONFIG
private var line = 1
private val indentContextStack: Deque = LinkedList()
private lateinit var stringTemplateIndenter: StringTemplateIndenter
override fun beforeFirstNode(editorConfig: EditorConfig) {
codeStyle = editorConfig[CODE_STYLE_PROPERTY]
indentConfig =
IndentConfig(
indentStyle = editorConfig[INDENT_STYLE_PROPERTY],
tabWidth = editorConfig[INDENT_SIZE_PROPERTY],
)
if (indentConfig.disabled) {
stopTraversalOfAST()
}
}
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
if (node.isRoot()) {
// File should not start with a whitespace
node
.nextLeaf()
?.takeIf { it.isWhiteSpaceWithoutNewline() }
?.let { whitespaceWithoutNewline ->
emit(node.startOffset, "Unexpected indentation", true)
if (autoCorrect) {
whitespaceWithoutNewline.treeParent.removeChild(whitespaceWithoutNewline)
}
}
indentContextStack.addLast(startNoIndentZone(node))
}
when {
node.isWhiteSpaceWithNewline() -> {
line++
if (indentContextStack.peekLast()?.activated == false) {
val lastIndentContext = indentContextStack.removeLast()
indentContextStack.addLast(
lastIndentContext.copy(activated = true),
)
}
visitNewLineIndentation(node, autoCorrect, emit)
}
node.elementType == CLASS_BODY ||
node.elementType == CONTEXT_RECEIVER_LIST ||
node.elementType == LONG_STRING_TEMPLATE_ENTRY ||
node.elementType == SUPER_TYPE_CALL_ENTRY ||
node.elementType == STRING_TEMPLATE ||
node.elementType == VALUE_ARGUMENT_LIST ->
startIndentContext(
fromAstNode = node,
lastChildIndent = "",
)
node.elementType == VALUE_ARGUMENT ->
visitValueArgument(node)
node.elementType == SECONDARY_CONSTRUCTOR ->
visitSecondaryConstructor(node)
node.elementType == PARENTHESIZED &&
node.treeParent.treeParent.elementType != IF ->
startIndentContext(node)
node.elementType == BINARY_WITH_TYPE ||
node.elementType == SUPER_TYPE_ENTRY ||
node.elementType == TYPE_ARGUMENT_LIST ||
node.elementType == TYPE_PARAMETER_LIST ||
node.elementType == USER_TYPE ->
startIndentContext(node)
node.elementType == DELEGATED_SUPER_TYPE_ENTRY ||
node.elementType == ANNOTATED_EXPRESSION ||
node.elementType == TYPE_REFERENCE ->
startIndentContext(
fromAstNode = node,
childIndent = "",
)
node.elementType == IF ->
visitIf(node)
node.elementType == LBRACE ->
visitLbrace(node)
node.elementType == VALUE_PARAMETER_LIST &&
node.treeParent.elementType != FUNCTION_LITERAL ->
startIndentContext(
fromAstNode = node,
lastChildIndent = "",
)
node.elementType == LPAR &&
node.nextCodeSibling()?.elementType == CONDITION ->
visitLparBeforeCondition(node)
node.elementType == VALUE_PARAMETER ->
visitValueParameter(node)
node.elementType == FUN ->
visitFun(node)
node.elementType == CLASS ->
visitClass(node)
node.elementType == OBJECT_DECLARATION ->
visitObjectDeclaration(node)
node.elementType == BINARY_EXPRESSION ->
visitBinaryExpression(node)
node.elementType in CHAINABLE_EXPRESSION -> {
if (codeStyle == ktlint_official &&
node.elementType == DOT_QUALIFIED_EXPRESSION &&
node.treeParent?.elementType == ARRAY_ACCESS_EXPRESSION &&
node.treeParent?.treeParent?.elementType == CALL_EXPRESSION
) {
// Issue 1540: Deviate and fix from incorrect formatting in IntelliJ IDEA formatting and produce following:
// val fooBar2 = foo
// .bar[0] {
// "foobar"
// }
startIndentContext(
fromAstNode = node.treeParent,
toAstNode = node.treeParent.treeParent.lastChildLeafOrSelf(),
)
} else if (node.prevCodeSibling().isElvisOperator()) {
startIndentContext(node)
} else if (node.treeParent.elementType in CHAINABLE_EXPRESSION) {
// Multiple dot qualified expressions and/or safe expression on the same line should not increase the indent level
} else {
startIndentContext(node)
}
}
node.elementType == IDENTIFIER &&
node.treeParent.elementType == PROPERTY ->
visitIdentifierInProperty(node)
node.elementType == LITERAL_STRING_TEMPLATE_ENTRY &&
node.nextCodeSibling()?.elementType == CLOSING_QUOTE ->
visitWhiteSpaceBeforeClosingQuote(node, autoCorrect, emit)
node.elementType == WHEN ->
visitWhen(node)
node.elementType == WHEN_ENTRY ->
visitWhenEntry(node)
node.elementType == WHERE_KEYWORD &&
node.nextCodeSibling()?.elementType == TYPE_CONSTRAINT_LIST ->
visitWhereKeywordBeforeTypeConstraintList(node)
node.elementType == KDOC ->
visitKdoc(node)
node.elementType == PROPERTY_ACCESSOR ||
node.elementType == TYPEALIAS ->
visitPropertyAccessor(node)
node.elementType == FOR ||
node.elementType == WHILE ->
visitConditionalLoop(node)
node.elementType == LBRACKET ->
visitLBracket(node)
node.elementType == NULLABLE_TYPE ->
visitNullableType(node)
node.elementType == DESTRUCTURING_DECLARATION ->
visitDestructuringDeclaration(node)
node.elementType == TRY ->
visitTryCatchFinally(node)
else -> {
LOGGER.trace { "No processing for ${node.elementType}: ${node.textWithEscapedTabAndNewline()}" }
}
}
}
private fun visitValueArgument(node: ASTNode) {
if (codeStyle == ktlint_official) {
// Deviate from standard IntelliJ IDEA formatting to allow formatting below:
// val foo = foo(
// parameterName =
// "The quick brown fox "
// .plus("jumps ")
// .plus("over the lazy dog"),
// )
startIndentContext(
fromAstNode = node,
lastChildIndent = "",
)
}
}
private fun visitSecondaryConstructor(node: ASTNode) {
node
.findChildByType(CONSTRUCTOR_DELEGATION_CALL)
?.let { constructorDelegationCall ->
val fromAstNode = node.skipLeadingWhitespaceCommentsAndAnnotations()
val nextToAstNode =
startIndentContext(
fromAstNode = constructorDelegationCall,
).prevCodeLeaf()
// Leading annotations and comments should be indented at same level as constructor itself
if (fromAstNode != node.nextLeaf()) {
startIndentContext(
fromAstNode = node,
toAstNode = nextToAstNode,
childIndent = "",
)
}
}
}
private fun visitIf(node: ASTNode) {
var nextToAstNode = node.lastChildLeafOrSelf()
node
.findChildByType(ELSE)
?.let { fromAstNode ->
nextToAstNode =
startIndentContext(
fromAstNode = fromAstNode,
toAstNode = nextToAstNode,
).prevCodeLeaf()
}
node
.findChildByType(THEN)
?.lastChildLeafOrSelf()
?.nextLeaf()
?.let { nodeAfterThenBlock ->
nextToAstNode =
startIndentContext(
fromAstNode = nodeAfterThenBlock,
toAstNode = nextToAstNode,
childIndent = "",
).prevCodeLeaf()
}
node
.findChildByType(RPAR)
?.nextCodeLeaf()
?.let { nodeAfterConditionBlock ->
nextToAstNode =
startIndentContext(
fromAstNode = nodeAfterConditionBlock,
toAstNode = nextToAstNode,
).prevCodeLeaf()
}
startIndentContext(
fromAstNode = node,
toAstNode = nextToAstNode,
lastChildIndent = "", // No indent for the RPAR
)
}
private fun visitLbrace(node: ASTNode) {
// Outer indent context
val rbrace =
requireNotNull(
node.nextSibling { it.elementType == RBRACE },
) { "Can not find matching rbrace" }
startIndentContext(
fromAstNode = node,
toAstNode = rbrace,
firstChildIndent = "",
lastChildIndent = "",
)
// Inner indent context in reversed order
node
.treeParent
?.takeIf { it.elementType == FUNCTION_LITERAL }
?.findChildByType(ARROW)
?.let { arrow ->
startIndentContext(
fromAstNode = arrow,
toAstNode = rbrace,
lastChildIndent = "",
)
startIndentContext(
fromAstNode = node,
toAstNode = arrow.prevCodeLeaf()!!,
childIndent = arrow.calculateIndentOfFunctionLiteralParameters(),
)
}
}
private fun ASTNode.calculateIndentOfFunctionLiteralParameters() =
if (codeStyle == ktlint_official || isFirstParameterOfFunctionLiteralPrecededByNewLine()) {
// val fieldExample =
// LongNameClass {
// paramA,
// paramB,
// paramC ->
// ClassB(paramA, paramB, paramC)
// }
indentConfig.indent.repeat(2)
} else {
// Allow default IntelliJ IDEA formatting:
// val fieldExample =
// LongNameClass { paramA,
// paramB,
// paramC ->
// ClassB(paramA, paramB, paramC)
// }
parent(CALL_EXPRESSION)
?.let { callExpression ->
val textBeforeFirstParameter =
callExpression.findChildByType(REFERENCE_EXPRESSION)?.text +
" { "
" ".repeat(textBeforeFirstParameter.length)
}
?: indentConfig.indent.repeat(2)
}
private fun ASTNode.isFirstParameterOfFunctionLiteralPrecededByNewLine() =
parent(FUNCTION_LITERAL)
?.findChildByType(VALUE_PARAMETER_LIST)
?.prevSibling { it.textContains('\n') } != null
private fun visitLparBeforeCondition(node: ASTNode) {
startIndentContext(
fromAstNode = requireNotNull(node.nextLeaf()), // Allow to pickup whitespace before condition
toAstNode = requireNotNull(node.nextCodeSibling()).lastChildLeafOrSelf(), // Ignore whitespace after condition but before rpar
nodeIndent = currentIndent() + indentConfig.indent,
childIndent = "",
)
}
private fun visitValueParameter(node: ASTNode) {
// Inner indent contexts in reversed order
var nextToAstNode: ASTNode = node.lastChildLeafOrSelf()
node
.findChildByType(EQ)
?.let { fromAstNode ->
nextToAstNode =
startIndentContext(
fromAstNode = fromAstNode,
toAstNode = nextToAstNode,
).prevCodeLeaf()
}
if (codeStyle == ktlint_official) {
// Deviate from standard IntelliJ IDEA formatting to allow formatting below:
// fun process(
// aVariableWithAVeryLongName:
// TypeWithAVeryLongNameThatDoesNotFitOnSameLineAsTheVariableName
// ): List
© 2015 - 2024 Weber Informatics LLC | Privacy Policy