From df436223a7221bd812201fb70d1a21bffb0126f9 Mon Sep 17 00:00:00 2001 From: paul-dingemans Date: Sun, 17 Jul 2022 21:54:58 +0200 Subject: [PATCH] Refactor life cycle hooks on Rule (#1547) Up until ktlint 0.46 the Rule class provided only one life cycle hook. This "visit" hook was called in a depth-first-approach on all nodes in the file. A rule like the IndentationRule used the RunOnRootOnly visitor modifier to call this lifecycle hook for the root node only in combination with an alternative way of traversing the ASTNodes. Downside of this approach was that suppression of the rule on blocks inside a file was not possible (#631). More generically, this applied to all rules, applying alternative traversals of the AST. The Rule class now offers new life cycle hooks: beforeFirstNode: This method is called once before the first node is visited. It can be used to initialize the state of the rule before processing of nodes starts. The ".editorconfig" properties (including overrides) are provided as parameter. beforeVisitChildNodes: This method is called on a node in AST before visiting its child nodes. This is repeated recursively for the child nodes resulting in a depth first traversal of the AST. This method is the equivalent of the "visit" life cycle hooks. However, note that in KtLint 0.48, the UserData of the rootnode no longer provides access to the ".editorconfig" properties. This method can be used to emit Lint Violations and to autocorrect if applicable. afterVisitChildNodes: This method is called on a node in AST after all its child nodes have been visited. This method can be used to emit Lint Violations and to autocorrect if applicable. afterLastNode: This method is called once after the last node in the AST is visited. It can be used for teardown of the state of the rule. The "visit" life cycle hook will be removed in Ktlint 0.48. In KtLint 0.47 the "visit" life cycle hook will be called only when hook "beforeVisitChildNodes" is not overridden. It is recommended to migrate to the new lifecycle hooks in KtLint 0.47. Please create an issue, in case you need additional assistence to implement the new life cycle hooks in your rules. Closes #631 Highlight of changes included: * Extend Rule hooks with beforeFirstNode, beforeVisitChildNodes, afterVisitChildNodes and afterLastNode and deprecating visit. * Simplification of logic - Remove node parameter from VisitorProvider result to simplify code - Simplify condition to rebuild supressed region locator - Checking for a suppressed region on the root node is not necessary. By default, the root node (e.g. the node with ElementType FILE) can not be suppressed. A '@file:Suppress' annotation on top of the file is already contained in a non-root node. - Remove indentation logic of complex arguments in ArgumentListWrappingRule and ParameterListWrappingRule as this is the responsibility of the IndentationRule * Remove concurrent visitor provider: Despite the name, the concurrent visitor is not faster than the sequential visitor as each file is processed on a single thread. There seem to be no other benefits for having two different visitor providers. Inlined the sequentialVisitor. * Fix early return when no rules have to be run * Extract PreparedCode and move PsiFileFactory to this vlass * Move logic to check whether to run on root node only or on all nodes from VisitorProvider to Ktlint. The visitor provider is only responsible for running the relevant rules in the correct order. On which node is to be executed is not relevant. * Deprecate the RunOnRootNodeOnly visitor modifier: This visitor modifier was typically used to run a custom node visit algorithm using the root node and the generic "package.visit" methods. The "package.visit" method however did not support rule suppression. With the new lifecycle hooks on the Rule class the visit modifier has become obsolete. * Refactor rules to comply with new life cycle hooks. Some rules needed a bigger refactoring to achieve this. * Provide EditorConfigProperties directly instead of via ASTNode UserData: Prepare to remove ".editorconfig" properties from the UserData in ktlint 0.48. Those properties are only required on the root node but the "getUserData" method can be called on any node. The new Rule lifecycle hooks "beforeFirstNode" provides the ".editorconfig" properties directly to the rule. This contract is cleaner and more explicit. --- CHANGELOG.md | 18 + .../com/pinterest/ktlint/core/KtLint.kt | 271 +++----- .../kotlin/com/pinterest/ktlint/core/Rule.kt | 64 +- .../core/api/UsesEditorConfigProperties.kt | 18 +- .../com/pinterest/ktlint/core/ast/package.kt | 2 + .../ktlint/core/internal/PreparedCode.kt | 91 +++ .../internal/SuppressionLocatorBuilder.kt | 12 +- .../ktlint/core/internal/VisitorProvider.kt | 90 +-- .../ktlint/core/DisabledRulesTest.kt | 2 +- .../com/pinterest/ktlint/core/KtLintTest.kt | 548 ++++++++++++---- .../core/UsesEditorConfigPropertiesTest.kt | 235 ++++--- .../ktlint/core/VisitorProviderTest.kt | 165 ++--- .../internal/EditorConfigGeneratorTest.kt | 2 +- .../core/internal/EditorConfigLoaderTest.kt | 2 +- .../ktlint/core/internal/RuleSorterTest.kt | 2 +- .../internal/SuppressionLocatorBuilderTest.kt | 2 +- .../BlockCommentInitialStarAlignmentRule.kt | 2 +- .../experimental/CommentWrappingRule.kt | 2 +- .../DiscouragedCommentLocationRule.kt | 2 +- .../experimental/FunKeywordSpacingRule.kt | 2 +- .../FunctionReturnTypeSpacingRule.kt | 2 +- .../experimental/FunctionSignatureRule.kt | 30 +- .../FunctionStartOfBodySpacingRule.kt | 2 +- .../FunctionTypeReferenceSpacingRule.kt | 2 +- .../ruleset/experimental/KdocWrappingRule.kt | 2 +- .../experimental/ModifierListSpacingRule.kt | 2 +- .../experimental/NullableTypeSpacingRule.kt | 2 +- .../experimental/ParameterListSpacingRule.kt | 2 +- ...enFunctionNameAndOpeningParenthesisRule.kt | 2 +- .../TypeArgumentListSpacingRule.kt | 2 +- .../TypeParameterListSpacingRule.kt | 2 +- ...saryParenthesesBeforeTrailingLambdaRule.kt | 2 +- .../ktlint/ruleset/standard/AnnotationRule.kt | 2 +- .../ruleset/standard/AnnotationSpacingRule.kt | 2 +- .../standard/ArgumentListWrappingRule.kt | 266 ++++---- .../ruleset/standard/ChainWrappingRule.kt | 2 +- .../ruleset/standard/CommentSpacingRule.kt | 2 +- .../ruleset/standard/EnumEntryNameCaseRule.kt | 2 +- .../ktlint/ruleset/standard/FilenameRule.kt | 82 ++- .../ruleset/standard/FinalNewlineRule.kt | 18 +- .../ruleset/standard/ImportOrderingRule.kt | 233 ++++--- .../ruleset/standard/IndentationRule.kt | 584 +++++++++--------- .../ruleset/standard/MaxLineLengthRule.kt | 25 +- .../ruleset/standard/ModifierOrderRule.kt | 2 +- .../ruleset/standard/MultiLineIfElseRule.kt | 2 +- .../standard/NoBlankLineBeforeRbraceRule.kt | 2 +- .../NoBlankLinesInChainedMethodCallsRule.kt | 2 +- .../standard/NoConsecutiveBlankLinesRule.kt | 2 +- .../ruleset/standard/NoEmptyClassBodyRule.kt | 2 +- .../NoEmptyFirstLineInMethodBlockRule.kt | 2 +- .../standard/NoLineBreakAfterElseRule.kt | 2 +- .../NoLineBreakBeforeAssignmentRule.kt | 2 +- .../ruleset/standard/NoMultipleSpacesRule.kt | 2 +- .../ruleset/standard/NoSemicolonsRule.kt | 2 +- .../ruleset/standard/NoTrailingSpacesRule.kt | 2 +- .../ruleset/standard/NoUnitReturnRule.kt | 2 +- .../ruleset/standard/NoUnusedImportsRule.kt | 328 ++++++---- .../ruleset/standard/NoWildcardImportsRule.kt | 15 +- .../ruleset/standard/PackageNameRule.kt | 2 +- .../standard/ParameterListWrappingRule.kt | 241 ++++---- .../SpacingAroundAngleBracketsRule.kt | 2 +- .../standard/SpacingAroundColonRule.kt | 2 +- .../standard/SpacingAroundCommaRule.kt | 2 +- .../standard/SpacingAroundCurlyRule.kt | 2 +- .../ruleset/standard/SpacingAroundDotRule.kt | 2 +- .../standard/SpacingAroundDoubleColonRule.kt | 2 +- .../standard/SpacingAroundKeywordRule.kt | 2 +- .../standard/SpacingAroundOperatorsRule.kt | 2 +- .../standard/SpacingAroundParensRule.kt | 2 +- .../SpacingAroundRangeOperatorRule.kt | 2 +- .../SpacingAroundUnaryOperatorRule.kt | 2 +- ...gBetweenDeclarationsWithAnnotationsRule.kt | 2 +- ...cingBetweenDeclarationsWithCommentsRule.kt | 2 +- .../ruleset/standard/StringTemplateRule.kt | 2 +- .../ruleset/standard/TrailingCommaRule.kt | 21 +- .../ktlint/ruleset/standard/WrappingRule.kt | 64 +- .../standard/ArgumentListWrappingRuleTest.kt | 2 + .../ruleset/standard/IndentationRuleTest.kt | 40 ++ .../standard/NoUnusedImportsRuleTest.kt | 10 +- .../src/main/kotlin/yourpkgname/NoVarRule.kt | 2 +- .../com/pinterest/ruleset/test/DumpASTRule.kt | 2 +- 81 files changed, 1999 insertions(+), 1584 deletions(-) create mode 100644 ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/PreparedCode.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 6298014413..03c9ec586b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,29 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Unreleased +### API Changes & RuleSet providers + +If you are not an API user nor a RuleSet provider, then you can safely skip this section. Otherwise, please read below carefully and upgrade your usage of ktlint. In this and coming releases, we are changing and adapting important parts of our API in order to increase maintainability and flexibility for future changes. Please avoid skipping a releases as that will make it harder to migrate. + +#### Rule lifecycle hooks / deprecate RunOnRootOnly visitor modifier + +Up until ktlint 0.46 the Rule class provided only one life cycle hook. This "visit" hook was called in a depth-first-approach on all nodes in the file. A rule like the IndentationRule used the RunOnRootOnly visitor modifier to call this lifecycle hook for the root node only in combination with an alternative way of traversing the ASTNodes. Downside of this approach was that suppression of the rule on blocks inside a file was not possible ([#631](https://github.com/pinterest/ktlint/issues/631)). More generically, this applied to all rules, applying alternative traversals of the AST. + +The Rule class now offers new life cycle hooks: +* beforeFirstNode: This method is called once before the first node is visited. It can be used to initialize the state of the rule before processing of nodes starts. The ".editorconfig" properties (including overrides) are provided as parameter. +* beforeVisitChildNodes: This method is called on a node in AST before visiting its child nodes. This is repeated recursively for the child nodes resulting in a depth first traversal of the AST. This method is the equivalent of the "visit" life cycle hooks. However, note that in KtLint 0.48, the UserData of the rootnode no longer provides access to the ".editorconfig" properties. This method can be used to emit Lint Violations and to autocorrect if applicable. +* afterVisitChildNodes: This method is called on a node in AST after all its child nodes have been visited. This method can be used to emit Lint Violations and to autocorrect if applicable. +* afterLastNode: This method is called once after the last node in the AST is visited. It can be used for teardown of the state of the rule. + +The "visit" life cycle hook will be removed in Ktlint 0.48. In KtLint 0.47 the "visit" life cycle hook will be called *only* when hook "beforeVisitChildNodes" is not overridden. It is recommended to migrate to the new lifecycle hooks in KtLint 0.47. Please create an issue, in case you need additional assistence to implement the new life cycle hooks in your rules. + + ### Added ### Fixed * Fix cli argument "--disabled_rules" ([#1520](https://github.com/pinterest/ktlint/issue/1520)). +* Disable/enable IndentationRule on blocks in middle of file. (`indent`) [#631](https://github.com/pinterest/ktlint/issues/631) ### Changed diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt index 2a53001c94..1bd2103315 100644 --- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt @@ -8,35 +8,29 @@ import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import com.pinterest.ktlint.core.internal.EditorConfigGenerator import com.pinterest.ktlint.core.internal.EditorConfigLoader -import com.pinterest.ktlint.core.internal.KotlinPsiFileFactoryProvider -import com.pinterest.ktlint.core.internal.LineAndColumn -import com.pinterest.ktlint.core.internal.SuppressionLocator +import com.pinterest.ktlint.core.internal.PreparedCode import com.pinterest.ktlint.core.internal.SuppressionLocatorBuilder import com.pinterest.ktlint.core.internal.VisitorProvider -import com.pinterest.ktlint.core.internal.buildPositionInTextLocator +import com.pinterest.ktlint.core.internal.prepareCodeForLinting import java.nio.file.FileSystems import java.nio.file.Path import java.nio.file.Paths import java.util.Locale import org.ec4j.core.model.PropertyType -import org.jetbrains.kotlin.com.intellij.lang.FileASTNode +import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.openapi.util.Key -import org.jetbrains.kotlin.com.intellij.psi.PsiElement -import org.jetbrains.kotlin.com.intellij.psi.PsiErrorElement -import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory -import org.jetbrains.kotlin.idea.KotlinLanguage -import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.utils.addToStdlib.safeAs public object KtLint { public val FILE_PATH_USER_DATA_KEY: Key = Key("FILE_PATH") + + @Deprecated("Marked for removal in Ktlint 0.48.0") public val EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY: Key = Key("EDITOR_CONFIG_PROPERTIES") - private const val UTF8_BOM = "\uFEFF" + internal const val UTF8_BOM = "\uFEFF" public const val STDIN_FILE: String = "" - private val kotlinPsiFileFactoryProvider = KotlinPsiFileFactoryProvider() - private val editorConfigLoader = EditorConfigLoader(FileSystems.getDefault()) + internal val editorConfigLoader = EditorConfigLoader(FileSystems.getDefault()) /** * @param fileName path of file to lint/format @@ -132,34 +126,15 @@ public object KtLint { * @throws RuleExecutionException in case of internal failure caused by a bug in rule implementation */ public fun lint(params: ExperimentalParams) { - val psiFileFactory = kotlinPsiFileFactoryProvider.getKotlinPsiFileFactory(params.isInvokedFromCli) - val preparedCode = prepareCodeForLinting(psiFileFactory, params) + val preparedCode = prepareCodeForLinting(params) val errors = mutableListOf() VisitorProvider(params) - .visitor(preparedCode.rootNode) - .invoke { node, rule, fqRuleId -> - // fixme: enforcing suppression based on node.startOffset is wrong - // (not just because not all nodes are leaves but because rules are free to emit (and fix!) errors at any position) - if ( - !preparedCode.suppressedRegionLocator(node.startOffset, fqRuleId, node === preparedCode.rootNode) - ) { - try { - rule.visit(node, false) { offset, errorMessage, canBeAutoCorrected -> - // https://github.com/shyiko/ktlint/issues/158#issuecomment-462728189 - if (node.startOffset != offset && - preparedCode.suppressedRegionLocator(offset, fqRuleId, node === preparedCode.rootNode) - ) { - return@visit - } - val (line, col) = preparedCode.positionInTextLocator(offset) - errors.add(LintError(line, col, fqRuleId, errorMessage, canBeAutoCorrected)) - } - } catch (e: Exception) { - val (line, col) = preparedCode.positionInTextLocator(node.startOffset) - - throw RuleExecutionException(line, col, fqRuleId, e) - } + .visitor(preparedCode.editorConfigProperties) + .invoke { rule, fqRuleId -> + preparedCode.executeRule(rule, fqRuleId, false) { offset, errorMessage, canBeAutoCorrected -> + val (line, col) = preparedCode.positionInTextLocator(offset) + errors.add(LintError(line, col, fqRuleId, errorMessage, canBeAutoCorrected)) } } @@ -168,59 +143,49 @@ public object KtLint { .forEach { e -> params.cb(e, false) } } - private fun prepareCodeForLinting( - psiFileFactory: PsiFileFactory, - params: ExperimentalParams - ): PreparedCode { - val normalizedText = normalizeText(params.text) - val positionInTextLocator = buildPositionInTextLocator(normalizedText) - - val psiFileName = if (params.script) "file.kts" else "file.kt" - val psiFile = psiFileFactory.createFileFromText( - psiFileName, - KotlinLanguage.INSTANCE, - normalizedText - ) as KtFile - - val errorElement = psiFile.findErrorElement() - if (errorElement != null) { - val (line, col) = positionInTextLocator(errorElement.textOffset) - throw ParseException(line, col, errorElement.errorDescription) - } - - val rootNode = psiFile.node - - val editorConfigProperties = editorConfigLoader.loadPropertiesForFile( - params.normalizedFilePath, - params.isStdIn, - params.editorConfigPath?.let { Paths.get(it) }, - params.rules, - params.editorConfigOverride, - params.debug - ) - - if (!params.isStdIn) { - rootNode.putUserData(FILE_PATH_USER_DATA_KEY, params.normalizedFilePath.toString()) - } - rootNode.putUserData(EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, editorConfigProperties) - - val suppressedRegionLocator = SuppressionLocatorBuilder.buildSuppressedRegionsLocator(rootNode) - - return PreparedCode( - rootNode, - positionInTextLocator, - suppressedRegionLocator - ) + private fun PreparedCode.executeRule( + rule: Rule, + fqRuleId: String, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + rule.beforeFirstNode(editorConfigProperties) + this.executeRuleOnNodeRecursively(rootNode, rule, fqRuleId, autoCorrect, emit) + rule.afterLastNode() } - @Deprecated( - message = "Should not be a part of public api. Will be removed in future release." - ) - public fun normalizeText(text: String): String { - return text - .replace("\r\n", "\n") - .replace("\r", "\n") - .replaceFirst(UTF8_BOM, "") + private fun PreparedCode.executeRuleOnNodeRecursively( + node: ASTNode, + rule: Rule, + fqRuleId: String, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + try { + rule.beforeVisitChildNodes(node, autoCorrect, emit) + if (!rule.runsOnRootNodeOnly()) { + node + .getChildren(null) + .forEach { childNode -> + // https://github.com/shyiko/ktlint/issues/158#issuecomment-462728189 + // fixme: enforcing suppression based on node.startOffset is wrong (not just because not all nodes are leaves + // but because rules are free to emit (and fix!) errors at any position) + if (!suppressedRegionLocator(childNode.startOffset, fqRuleId, childNode === rootNode)) { + this.executeRuleOnNodeRecursively(childNode, rule, fqRuleId, autoCorrect, emit) + } + } + } + rule.afterVisitChildNodes(node, autoCorrect, emit) + } catch (e: Exception) { + if (autoCorrect) { + // line/col cannot be reliably mapped as exception might originate from a node not present in the + // original AST + throw RuleExecutionException(0, 0, fqRuleId, e) + } else { + val (line, col) = positionInTextLocator(node.startOffset) + throw RuleExecutionException(line, col, fqRuleId, e) + } + } } /** @@ -231,93 +196,53 @@ public object KtLint { */ public fun format(params: ExperimentalParams): String { val hasUTF8BOM = params.text.startsWith(UTF8_BOM) - val psiFileFactory = kotlinPsiFileFactoryProvider.getKotlinPsiFileFactory(params.isInvokedFromCli) - val preparedCode = prepareCodeForLinting(psiFileFactory, params) + val preparedCode = prepareCodeForLinting(params) var tripped = false var mutated = false val errors = mutableSetOf>() val visitorProvider = VisitorProvider(params = params) visitorProvider - .visitor( - preparedCode.rootNode, - concurrent = false - ).invoke { node, rule, fqRuleId -> - // fixme: enforcing suppression based on node.startOffset is wrong - // (not just because not all nodes are leaves but because rules are free to emit (and fix!) errors at any position) - if ( - !preparedCode.suppressedRegionLocator(node.startOffset, fqRuleId, node === preparedCode.rootNode) - ) { - try { - rule.visit(node, true) { offset, errorMessage, canBeAutoCorrected -> - tripped = true - if (canBeAutoCorrected) { - mutated = true - if (preparedCode.suppressedRegionLocator !== SuppressionLocatorBuilder.noSuppression) { - preparedCode.suppressedRegionLocator = - SuppressionLocatorBuilder.buildSuppressedRegionsLocator( - preparedCode.rootNode - ) - } - } - val (line, col) = preparedCode.positionInTextLocator(offset) - errors.add( - Pair( - LintError(line, col, fqRuleId, errorMessage, canBeAutoCorrected), - // It is assumed that a rule that emits that an error can be autocorrected, also - // does correct the error. - canBeAutoCorrected + .visitor(preparedCode.editorConfigProperties) + .invoke { rule, fqRuleId -> + preparedCode.executeRule(rule, fqRuleId, true) { offset, errorMessage, canBeAutoCorrected -> + tripped = true + if (canBeAutoCorrected) { + mutated = true + if (preparedCode.suppressedRegionLocator !== SuppressionLocatorBuilder.noSuppression) { + // Offsets of start and end positions of suppressed regions might have changed due to + // updating the code + preparedCode.suppressedRegionLocator = + SuppressionLocatorBuilder.buildSuppressedRegionsLocator( + preparedCode.rootNode ) - ) } - } catch (e: Exception) { - // line/col cannot be reliably mapped as exception might originate from a node not present - // in the original AST - throw RuleExecutionException(0, 0, fqRuleId, e) } + val (line, col) = preparedCode.positionInTextLocator(offset) + errors.add( + Pair( + LintError(line, col, fqRuleId, errorMessage, canBeAutoCorrected), + // It is assumed that a rule that emits that an error can be autocorrected, also + // does correct the error. + canBeAutoCorrected + ) + ) } } if (tripped) { visitorProvider - .visitor(preparedCode.rootNode) - .invoke { node, rule, fqRuleId -> - // fixme: enforcing suppression based on node.startOffset is wrong - // (not just because not all nodes are leaves but because rules are free to emit (and fix!) errors at any position) - if ( - !preparedCode.suppressedRegionLocator( - node.startOffset, - fqRuleId, - node === preparedCode.rootNode + .visitor(preparedCode.editorConfigProperties) + .invoke { rule, fqRuleId -> + preparedCode.executeRule(rule, fqRuleId, false) { offset, errorMessage, canBeAutoCorrected -> + val (line, col) = preparedCode.positionInTextLocator(offset) + errors.add( + Pair( + LintError(line, col, fqRuleId, errorMessage, canBeAutoCorrected), + // It is assumed that a rule only corrects an error after it has emitted an + // error and indicating that it actually can be autocorrected. + false + ) ) - ) { - try { - rule.visit(node, false) { offset, errorMessage, canBeAutoCorrected -> - // https://github.com/shyiko/ktlint/issues/158#issuecomment-462728189 - if ( - node.startOffset != offset && - preparedCode.suppressedRegionLocator( - offset, - fqRuleId, - node === preparedCode.rootNode - ) - ) { - return@visit - } - val (line, col) = preparedCode.positionInTextLocator(offset) - errors.add( - Pair( - LintError(line, col, fqRuleId, errorMessage, canBeAutoCorrected), - // It is assumed that a rule only corrects an error after it has emitted an - // error and indicating that it actually can be autocorrected. - false - ) - ) - } - } catch (e: Exception) { - val (line, col) = preparedCode.positionInTextLocator(node.startOffset) - - throw RuleExecutionException(line, col, fqRuleId, e) - } } } } @@ -341,6 +266,9 @@ public object KtLint { } } + private fun Rule.runsOnRootNodeOnly() = + visitorModifiers.contains(Rule.VisitorModifier.RunOnRootNodeOnly) + /** * Reduce memory usage of all internal caches. */ @@ -393,23 +321,4 @@ public object KtLint { else -> "\n" } } - - private fun PsiElement.findErrorElement(): PsiErrorElement? { - if (this is PsiErrorElement) { - return this - } - this.children.forEach { child -> - val errorElement = child.findErrorElement() - if (errorElement != null) { - return errorElement - } - } - return null - } - - private class PreparedCode( - val rootNode: FileASTNode, - val positionInTextLocator: (offset: Int) -> LineAndColumn, - var suppressedRegionLocator: SuppressionLocator - ) } diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/Rule.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/Rule.kt index 12997197cd..ff9b5f4e7a 100644 --- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/Rule.kt +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/Rule.kt @@ -1,5 +1,6 @@ package com.pinterest.ktlint.core +import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.internal.IdNamingPolicy import org.jetbrains.kotlin.com.intellij.lang.ASTNode @@ -15,7 +16,7 @@ import org.jetbrains.kotlin.com.intellij.lang.ASTNode * * @see RuleSet */ -abstract class Rule( +public open class Rule( val id: String, public val visitorModifiers: Set = emptySet() ) { @@ -24,17 +25,68 @@ abstract class Rule( } /** - * This method is going to be executed for each node in AST (in DFS fashion). + * This method is called once before the first node is visited. It can be used to initialize the state of the rule + * before processing of nodes starts. + */ + @Suppress("UNUSED_PARAMETER") + public open fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) {} + + /** + * This method is called on a node in AST before visiting the child nodes. This is repeated recursively for the + * child nodes resulting in a depth first traversal of the AST. * * @param node AST node * @param autoCorrect indicates whether rule should attempt auto-correction * @param emit a way for rule to notify about a violation (lint error) */ - abstract fun visit( + public open fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) = + /** + * For backwards compatibility with ktlint 0.46.x or before, call [visit] when not implemented on node. + * Add abstract function modifier and remove function body after removal of deprecated [visit] method to enforce + * explicit implementation by rule developer. + */ + visit(node, autoCorrect, emit) + + /** + * Rules that override method [visit] should rename that method to [beforeVisitChildNodes]. For backwards + * compatibility reasons (in KtLint 0.47 only), this method is called via the default implementation of + * [beforeVisitChildNodes]. Whenever [beforeVisitChildNodes] is overridden with a custom implementation, this method + * will no longer be called. + * + * @param node AST node + * @param autoCorrect indicates whether rule should attempt auto-correction + * @param emit a way for rule to notify about a violation (lint error) + */ + @Deprecated( + message = "Marked for deletion in ktlint 0.48.0", + replaceWith = ReplaceWith("beforeVisitChildNodes(node, autocorrect, emit") ) + @Suppress("UNUSED_PARAMETER") + public open fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) {} + + /** + * This method is called on a node in AST after all its child nodes have been visited. + */ + @Suppress("unused", "UNUSED_PARAMETER") + public open fun afterVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) {} + + /** + * This method is called once after the last node in the AST is visited. It can be used for teardown of the state + * of the rule. + */ + public open fun afterLastNode() {} sealed class VisitorModifier { @@ -56,6 +108,12 @@ abstract class Rule( object RunAsLateAsPossible : VisitorModifier() + @Deprecated( + """ + Marked for removal in Ktlint 0.48. This modifier blocks the ability to suppress ktlint rules. See + changelog Ktlint 0.47 for details on how to modify a rule using this modifier. + """ + ) object RunOnRootNodeOnly : VisitorModifier() } } diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/api/UsesEditorConfigProperties.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/api/UsesEditorConfigProperties.kt index 7550470d86..c3cb3e2f77 100644 --- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/api/UsesEditorConfigProperties.kt +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/api/UsesEditorConfigProperties.kt @@ -5,6 +5,7 @@ import com.pinterest.ktlint.core.KtLint import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.CodeStyleValue import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.CodeStyleValue.android import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.CodeStyleValue.official +import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.codeStyleSetProperty import com.pinterest.ktlint.core.initKtLintKLogger import mu.KotlinLogging import org.ec4j.core.model.Property @@ -42,18 +43,31 @@ public interface UsesEditorConfigProperties { * Get the value of [EditorConfigProperty] based on loaded [EditorConfigProperties] content for the current * [ASTNode]. */ + public fun EditorConfigProperties.getEditorConfigValue(editorConfigProperty: EditorConfigProperty): T { + require(editorConfigProperties.contains(editorConfigProperty)) { + "EditorConfigProperty '${editorConfigProperty.type.name}' may only be retrieved when it is registered in the editorConfigProperties." + } + val codeStyle = getEditorConfigValue(codeStyleSetProperty, official) + return getEditorConfigValue(editorConfigProperty, codeStyle) + } + + /** + * Get the value of [EditorConfigProperty] based on loaded [EditorConfigProperties] content for the current + * [ASTNode]. + */ + @Deprecated(message = "Marked for deletion in Ktlint 0.48. EditorConfigProperties are now supplied to Rule via call on method beforeFirstNode") public fun ASTNode.getEditorConfigValue(editorConfigProperty: EditorConfigProperty): T { require(editorConfigProperties.contains(editorConfigProperty)) { "EditorConfigProperty '${editorConfigProperty.type.name}' may only be retrieved when it is registered in the editorConfigProperties." } val editorConfigPropertyValues = getUserData(KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY)!! - val codeStyle = editorConfigPropertyValues.getEditorConfigValue(DefaultEditorConfigProperties.codeStyleSetProperty) + val codeStyle = editorConfigPropertyValues.getEditorConfigValue(codeStyleSetProperty, official) return editorConfigPropertyValues.getEditorConfigValue(editorConfigProperty, codeStyle) } private fun EditorConfigProperties.getEditorConfigValue( editorConfigProperty: EditorConfigProperty, - codeStyleValue: CodeStyleValue = official + codeStyleValue: CodeStyleValue ): T { val property = get(editorConfigProperty.type.name) diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/ast/package.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/ast/package.kt index 8fc9a91782..a0c00c608a 100644 --- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/ast/package.kt +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/ast/package.kt @@ -232,11 +232,13 @@ fun LeafElement.upsertWhitespaceAfterMe(text: String): LeafElement { } } +@Deprecated(message = "Marked for removal in Ktlint 0.48. See Ktlint 0.47.0 changelog for more information.") fun ASTNode.visit(enter: (node: ASTNode) -> Unit) { enter(this) this.getChildren(null).forEach { it.visit(enter) } } +@Deprecated(message = "Marked for removal in Ktlint 0.48. See Ktlint 0.47.0 changelog for more information.") fun ASTNode.visit(enter: (node: ASTNode) -> Unit, exit: (node: ASTNode) -> Unit) { enter(this) this.getChildren(null).forEach { it.visit(enter, exit) } diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/PreparedCode.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/PreparedCode.kt new file mode 100644 index 0000000000..40a7f14fa8 --- /dev/null +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/PreparedCode.kt @@ -0,0 +1,91 @@ +package com.pinterest.ktlint.core.internal + +import com.pinterest.ktlint.core.KtLint +import com.pinterest.ktlint.core.ParseException +import com.pinterest.ktlint.core.api.EditorConfigProperties +import java.nio.file.Paths +import org.jetbrains.kotlin.com.intellij.lang.FileASTNode +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.com.intellij.psi.PsiErrorElement +import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.psi.KtFile + +private val kotlinPsiFileFactoryProvider = KotlinPsiFileFactoryProvider() + +internal class PreparedCode( + val rootNode: FileASTNode, + val editorConfigProperties: EditorConfigProperties, + val positionInTextLocator: (offset: Int) -> LineAndColumn, + var suppressedRegionLocator: SuppressionLocator +) + +internal fun prepareCodeForLinting(params: KtLint.ExperimentalParams): PreparedCode { + val psiFileFactory = kotlinPsiFileFactoryProvider.getKotlinPsiFileFactory(params.isInvokedFromCli) + val normalizedText = normalizeText(params.text) + val positionInTextLocator = buildPositionInTextLocator(normalizedText) + + val psiFileName = if (params.script) { + "file.kts" + } else { + "file.kt" + } + val psiFile = psiFileFactory.createFileFromText( + psiFileName, + KotlinLanguage.INSTANCE, + normalizedText + ) as KtFile + + val errorElement = psiFile.findErrorElement() + if (errorElement != null) { + val (line, col) = positionInTextLocator(errorElement.textOffset) + throw ParseException(line, col, errorElement.errorDescription) + } + + val rootNode = psiFile.node + + val editorConfigProperties = KtLint.editorConfigLoader.loadPropertiesForFile( + params.normalizedFilePath, + params.isStdIn, + params.editorConfigPath?.let { Paths.get(it) }, + params.rules, + params.editorConfigOverride, + params.debug + ) + + if (!params.isStdIn) { + rootNode.putUserData(KtLint.FILE_PATH_USER_DATA_KEY, params.normalizedFilePath.toString()) + } + + // Keep for backwards compatibility in Ktlint 0.47.0 until ASTNode.getEditorConfigValue in UsesEditorConfigProperties + // is removed + rootNode.putUserData(KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, editorConfigProperties) + + val suppressedRegionLocator = SuppressionLocatorBuilder.buildSuppressedRegionsLocator(rootNode) + + return PreparedCode( + rootNode, + editorConfigProperties, + positionInTextLocator, + suppressedRegionLocator + ) +} + +private fun normalizeText(text: String): String { + return text + .replace("\r\n", "\n") + .replace("\r", "\n") + .replaceFirst(KtLint.UTF8_BOM, "") +} + +private fun PsiElement.findErrorElement(): PsiErrorElement? { + if (this is PsiErrorElement) { + return this + } + this.children.forEach { child -> + val errorElement = child.findErrorElement() + if (errorElement != null) { + return errorElement + } + } + return null +} diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/SuppressionLocatorBuilder.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/SuppressionLocatorBuilder.kt index bdc542ff82..693e08a8c4 100644 --- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/SuppressionLocatorBuilder.kt +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/SuppressionLocatorBuilder.kt @@ -1,7 +1,6 @@ package com.pinterest.ktlint.core.internal import com.pinterest.ktlint.core.ast.prevLeaf -import com.pinterest.ktlint.core.ast.visit import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiComment import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace @@ -60,7 +59,7 @@ internal object SuppressionLocatorBuilder { ): List { val result = ArrayList() val open = ArrayList() - rootNode.visit { node -> + rootNode.collect { node -> if (node is PsiComment) { val text = node.getText() if (text.startsWith("//")) { @@ -85,7 +84,7 @@ internal object SuppressionLocatorBuilder { val openingHint = open.removeAt(openHintIndex) result.add( SuppressionHint( - IntRange(openingHint.range.first, node.startOffset), + IntRange(openingHint.range.first, node.startOffset - 1), disabledRules ) ) @@ -110,6 +109,13 @@ internal object SuppressionLocatorBuilder { return result } + private fun ASTNode.collect(block: (node: ASTNode) -> Unit) { + block(this) + this + .getChildren(null) + .forEach { it.collect(block) } + } + private fun parseHintArgs( commentText: String, key: String diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/VisitorProvider.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/VisitorProvider.kt index edebdbc3f7..8f591b721e 100644 --- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/VisitorProvider.kt +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/VisitorProvider.kt @@ -3,10 +3,9 @@ package com.pinterest.ktlint.core.internal import com.pinterest.ktlint.core.KtLint import com.pinterest.ktlint.core.Rule import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.disabledRulesProperty +import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import com.pinterest.ktlint.core.api.UsesEditorConfigProperties.EditorConfigProperty -import com.pinterest.ktlint.core.ast.visit -import org.jetbrains.kotlin.com.intellij.lang.ASTNode /** * The VisitorProvider is created for each file being scanned. As the [RuleSorter] logs the order in which the rules are @@ -34,26 +33,25 @@ internal class VisitorProvider( ruleSorter }.getSortedRules(params.ruleSets, params.debug) - internal fun visitor( - rootNode: ASTNode, - concurrent: Boolean = true - ): ((node: ASTNode, rule: Rule, fqRuleId: String) -> Unit) -> Unit { + internal fun visitor(editorConfigProperties: EditorConfigProperties): ((rule: Rule, fqRuleId: String) -> Unit) -> Unit { val enabledRuleReferences = ruleReferences - .filter { ruleReference -> isNotDisabled(rootNode, ruleReference.toQualifiedRuleId()) } + .filter { ruleReference -> isNotDisabled(editorConfigProperties, ruleReference.toQualifiedRuleId()) } val enabledQualifiedRuleIds = enabledRuleReferences.map { it.toQualifiedRuleId() } val enabledRules = params.ruleSets .flatMap { ruleSet -> ruleSet .rules .filter { rule -> toQualifiedRuleId(ruleSet.id, rule.id) in enabledQualifiedRuleIds } - .filter { rule -> isNotDisabled(rootNode, toQualifiedRuleId(ruleSet.id, rule.id)) } + .filter { rule -> isNotDisabled(editorConfigProperties, toQualifiedRuleId(ruleSet.id, rule.id)) } .map { rule -> toQualifiedRuleId(ruleSet.id, rule.id) to rule } }.toMap() - if (params.debug && enabledRules.isEmpty()) { - println( - "[DEBUG] Skipping file as no enabled rules are found to be executed" - ) + if (enabledRules.isEmpty()) { + if (params.debug && enabledRules.isEmpty()) { + println( + "[DEBUG] Skipping file as no enabled rules are found to be executed" + ) + } return { _ -> } } val ruleReferencesToBeSkipped = @@ -74,65 +72,39 @@ internal class VisitorProvider( } } val ruleReferenceWithoutEntriesToBeSkipped = enabledRuleReferences - ruleReferencesToBeSkipped.toSet() - if (params.debug && ruleReferenceWithoutEntriesToBeSkipped.isEmpty()) { - println( - "[DEBUG] Skipping file as no enabled rules are found to be executed" - ) + if (ruleReferenceWithoutEntriesToBeSkipped.isEmpty()) { + if (params.debug) { + println( + "[DEBUG] Skipping file as no enabled rules are found to be executed" + ) + } return { _ -> } } - return if (concurrent) { - concurrentVisitor(enabledRules, ruleReferenceWithoutEntriesToBeSkipped, rootNode) - } else { - sequentialVisitor(enabledRules, ruleReferenceWithoutEntriesToBeSkipped, rootNode) - } - } - - private fun concurrentVisitor( - enabledRules: Map, - ruleReferences: List, - rootNode: ASTNode - ): ((node: ASTNode, rule: Rule, fqRuleId: String) -> Unit) -> Unit { - return { visit -> - rootNode.visit { node -> - ruleReferences - .forEach { ruleReference -> - if (node == rootNode || !ruleReference.runOnRootNodeOnly) { - enabledRules[ruleReference.toQualifiedRuleId()] - ?.let { rule -> - visit(node, rule, ruleReference.toShortenedQualifiedRuleId()) - } - } + val rules = ruleReferences + .mapNotNull { ruleReference -> + enabledRules[ruleReference.toQualifiedRuleId()] + ?.let { + ShortenedQualifiedRule(ruleReference.toShortenedQualifiedRuleId(), it) } } - } - } - - private fun sequentialVisitor( - enabledRules: Map, - ruleReferences: List, - rootNode: ASTNode - ): ((node: ASTNode, rule: Rule, fqRuleId: String) -> Unit) -> Unit { return { visit -> - ruleReferences - .forEach { ruleReference -> - enabledRules[ruleReference.toQualifiedRuleId()] - ?.let { rule -> - if (ruleReference.runOnRootNodeOnly) { - visit(rootNode, rule, ruleReference.toShortenedQualifiedRuleId()) - } else { - rootNode.visit { node -> visit(node, rule, ruleReference.toShortenedQualifiedRuleId()) } - } - } - } + rules.forEach { + visit(it.rule, it.shortenedQualifiedRuleId) + } } } - private fun isNotDisabled(rootNode: ASTNode, qualifiedRuleId: String): Boolean = - rootNode + private fun isNotDisabled(editorConfigProperties: EditorConfigProperties, qualifiedRuleId: String): Boolean = + editorConfigProperties .getEditorConfigValue(disabledRulesProperty) .split(",") .none { // The rule set id in the disabled_rules setting may be omitted for rules in the standard rule set it.toQualifiedRuleId() == qualifiedRuleId } + + private data class ShortenedQualifiedRule( + val shortenedQualifiedRuleId: String, + val rule: Rule + ) } diff --git a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/DisabledRulesTest.kt b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/DisabledRulesTest.kt index 4ab7849441..c7c4011a62 100644 --- a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/DisabledRulesTest.kt +++ b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/DisabledRulesTest.kt @@ -60,7 +60,7 @@ class DisabledRulesTest { } class NoVarRule : Rule("no-var") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/KtLintTest.kt b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/KtLintTest.kt index df5d929133..8da28cd694 100644 --- a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/KtLintTest.kt +++ b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/KtLintTest.kt @@ -2,7 +2,20 @@ package com.pinterest.ktlint.core import com.pinterest.ktlint.core.AutoCorrectErrorRule.Companion.STRING_VALUE_AFTER_AUTOCORRECT import com.pinterest.ktlint.core.DummyRuleWithCustomEditorConfigProperty.Companion.SOME_CUSTOM_RULE_PROPERTY +import com.pinterest.ktlint.core.Rule.VisitorModifier.RunAsLateAsPossible +import com.pinterest.ktlint.core.Rule.VisitorModifier.RunOnRootNodeOnly +import com.pinterest.ktlint.core.RuleExecutionCall.RuleMethod.AFTER_CHILDREN +import com.pinterest.ktlint.core.RuleExecutionCall.RuleMethod.AFTER_LAST +import com.pinterest.ktlint.core.RuleExecutionCall.RuleMethod.BEFORE_CHILDREN +import com.pinterest.ktlint.core.RuleExecutionCall.RuleMethod.BEFORE_FIRST +import com.pinterest.ktlint.core.RuleExecutionCall.RuleMethod.VISIT +import com.pinterest.ktlint.core.RuleExecutionCall.VisitNodeType.CHILD +import com.pinterest.ktlint.core.RuleExecutionCall.VisitNodeType.ROOT +import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.api.UsesEditorConfigProperties +import com.pinterest.ktlint.core.ast.ElementType.FILE +import com.pinterest.ktlint.core.ast.ElementType.IMPORT_LIST +import com.pinterest.ktlint.core.ast.ElementType.PACKAGE_DIRECTIVE import com.pinterest.ktlint.core.ast.ElementType.REGULAR_STRING_PART import com.pinterest.ktlint.core.ast.isRoot import org.assertj.core.api.Assertions.assertThat @@ -10,6 +23,8 @@ import org.assertj.core.api.Assertions.assertThatThrownBy import org.ec4j.core.model.PropertyType import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement +import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType +import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -331,128 +346,367 @@ class KtLintTest { ) } } + } + + @Test + fun `Given a rule returning an errors which can and can not be autocorrected than that state of the error can be retrieved in the callback`() { + val code = + """ + val foo = "${AutoCorrectErrorRule.STRING_VALUE_NOT_TO_BE_CORRECTED}" + val bar = "${AutoCorrectErrorRule.STRING_VALUE_TO_BE_AUTOCORRECTED}" + """.trimIndent() + val formattedCode = + """ + val foo = "${AutoCorrectErrorRule.STRING_VALUE_NOT_TO_BE_CORRECTED}" + val bar = "$STRING_VALUE_AFTER_AUTOCORRECT" + """.trimIndent() + val callbacks = mutableListOf() + val actualFormattedCode = KtLint.format( + KtLint.ExperimentalParams( + text = code, + ruleSets = listOf( + RuleSet("standard", AutoCorrectErrorRule()) + ), + userData = emptyMap(), + cb = { e, corrected -> + callbacks.add( + CallbackResult( + line = e.line, + col = e.col, + ruleId = e.ruleId, + detail = e.detail, + canBeAutoCorrected = e.canBeAutoCorrected, + corrected = corrected + ) + ) + }, + script = false, + editorConfigPath = null, + debug = false + ) + ) + assertThat(actualFormattedCode).isEqualTo(formattedCode) + assertThat(callbacks).containsExactly( + CallbackResult( + line = 1, + col = 12, + ruleId = "auto-correct", + detail = AutoCorrectErrorRule.ERROR_MESSAGE_CAN_NOT_BE_AUTOCORRECTED, + canBeAutoCorrected = false, + corrected = false + ), + CallbackResult( + line = 2, + col = 12, + ruleId = "auto-correct", + detail = AutoCorrectErrorRule.ERROR_MESSAGE_CAN_BE_AUTOCORRECTED, + canBeAutoCorrected = true, + corrected = true + ) + ) + } + @DisplayName("Calls to rules defined in ktlint 0.46.x or before") + @Nested + inner class RuleExecutionCallsLegacy { @Test - fun `Given a rule returning an errors which can and can not be autocorrected than that state of the error can be retrieved in the callback`() { - val code = - """ - val foo = "${AutoCorrectErrorRule.STRING_VALUE_NOT_TO_BE_CORRECTED}" - val bar = "${AutoCorrectErrorRule.STRING_VALUE_TO_BE_AUTOCORRECTED}" - """.trimIndent() - val formattedCode = - """ - val foo = "${AutoCorrectErrorRule.STRING_VALUE_NOT_TO_BE_CORRECTED}" - val bar = "$STRING_VALUE_AFTER_AUTOCORRECT" - """.trimIndent() - val callbacks = mutableListOf() - val actualFormattedCode = KtLint.format( + fun `Given a normal rule then execute on root node and child nodes`() { + val ruleExecutionCalls = mutableListOf() + KtLint.lint( KtLint.ExperimentalParams( - text = code, + // An empty file results in nodes with elementTypes FILE, PACKAGE_DIRECTIVE and IMPORT_LIST respectively + text = "", ruleSets = listOf( - RuleSet("standard", AutoCorrectErrorRule()) + RuleSet( + "standard", + SimpleTestRuleLegacy( + ruleExecutionCalls = ruleExecutionCalls, + id = "a", + visitorModifiers = setOf() + ), + SimpleTestRuleLegacy( + ruleExecutionCalls = ruleExecutionCalls, + id = "b", + visitorModifiers = setOf(Rule.VisitorModifier.RunAsLateAsPossible) + ) + ) ), - userData = emptyMap(), - cb = { e, corrected -> - callbacks.add( - CallbackResult( - line = e.line, - col = e.col, - ruleId = e.ruleId, - detail = e.detail, - canBeAutoCorrected = e.canBeAutoCorrected, - corrected = corrected + cb = { _, _ -> } + ) + ) + assertThat(ruleExecutionCalls).containsExactly( + RuleExecutionCall(VISIT, ROOT, "a", FILE), + RuleExecutionCall(VISIT, CHILD, "a", PACKAGE_DIRECTIVE), + RuleExecutionCall(VISIT, CHILD, "a", IMPORT_LIST), + RuleExecutionCall(VISIT, ROOT, "b", FILE), + RuleExecutionCall(VISIT, CHILD, "b", PACKAGE_DIRECTIVE), + RuleExecutionCall(VISIT, CHILD, "b", IMPORT_LIST) + ) + } + + @Test + fun `Given a run-on-root-node-only rule then execute on root node but not on child nodes`() { + val ruleExecutionCalls = mutableListOf() + KtLint.lint( + KtLint.ExperimentalParams( + text = "fun main() {}", + ruleSets = listOf( + RuleSet( + "standard", + SimpleTestRuleLegacy( + ruleExecutionCalls = ruleExecutionCalls, + id = "a", + visitorModifiers = setOf(Rule.VisitorModifier.RunOnRootNodeOnly) + ), + SimpleTestRuleLegacy( + ruleExecutionCalls = ruleExecutionCalls, + id = "b", + visitorModifiers = setOf( + Rule.VisitorModifier.RunOnRootNodeOnly, + Rule.VisitorModifier.RunAsLateAsPossible + ) ) ) - }, - script = false, - editorConfigPath = null, - debug = false + ), + cb = { _, _ -> } ) ) - assertThat(actualFormattedCode).isEqualTo(formattedCode) - assertThat(callbacks).containsExactly( - CallbackResult( - line = 1, - col = 12, - ruleId = "auto-correct", - detail = AutoCorrectErrorRule.ERROR_MESSAGE_CAN_NOT_BE_AUTOCORRECTED, - canBeAutoCorrected = false, - corrected = false - ), - CallbackResult( - line = 2, - col = 12, - ruleId = "auto-correct", - detail = AutoCorrectErrorRule.ERROR_MESSAGE_CAN_BE_AUTOCORRECTED, - canBeAutoCorrected = true, - corrected = true + assertThat(ruleExecutionCalls).containsExactly( + RuleExecutionCall(VISIT, ROOT, "a", FILE), + RuleExecutionCall(VISIT, ROOT, "b", FILE) + ) + } + + @Test + fun `Given multiple rules which have to run in a certain order`() { + val ruleExecutionCalls = mutableListOf() + KtLint.lint( + KtLint.ExperimentalParams( + // An empty file results in nodes with elementTypes FILE, PACKAGE_DIRECTIVE and IMPORT_LIST respectively + text = "", + ruleSets = listOf( + RuleSet( + "standard", + SimpleTestRuleLegacy( + ruleExecutionCalls = ruleExecutionCalls, + id = "e", + visitorModifiers = setOf(Rule.VisitorModifier.RunAsLateAsPossible) + ), + SimpleTestRuleLegacy( + ruleExecutionCalls = ruleExecutionCalls, + id = "d", + visitorModifiers = setOf( + Rule.VisitorModifier.RunOnRootNodeOnly, + Rule.VisitorModifier.RunAsLateAsPossible + ) + ), + SimpleTestRuleLegacy( + ruleExecutionCalls = ruleExecutionCalls, + id = "b" + ), + SimpleTestRuleLegacy( + ruleExecutionCalls = ruleExecutionCalls, + id = "a", + visitorModifiers = setOf( + Rule.VisitorModifier.RunOnRootNodeOnly + ) + ), + SimpleTestRuleLegacy( + ruleExecutionCalls = ruleExecutionCalls, + id = "c" + ) + ) + ), + cb = { _, _ -> } ) ) + assertThat(ruleExecutionCalls).containsExactly( + RuleExecutionCall(VISIT, ROOT, "a", FILE), + RuleExecutionCall(VISIT, ROOT, "b", FILE), + RuleExecutionCall(VISIT, CHILD, "b", PACKAGE_DIRECTIVE), + RuleExecutionCall(VISIT, CHILD, "b", IMPORT_LIST), + RuleExecutionCall(VISIT, ROOT, "c", FILE), + RuleExecutionCall(VISIT, CHILD, "c", PACKAGE_DIRECTIVE), + RuleExecutionCall(VISIT, CHILD, "c", IMPORT_LIST), + RuleExecutionCall(VISIT, ROOT, "d", FILE), + RuleExecutionCall(VISIT, ROOT, "e", FILE), + RuleExecutionCall(VISIT, CHILD, "e", PACKAGE_DIRECTIVE), + RuleExecutionCall(VISIT, CHILD, "e", IMPORT_LIST) + ) } } - @Test - fun testRuleExecutionOrder() { - open class R( - private val bus: MutableList, - id: String, - visitorModifiers: Set = emptySet() - ) : Rule(id, visitorModifiers) { - private var done = false - override fun visit( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit - ) { - if (node.isRoot()) { - bus.add("file:$id") - } else if (!done) { - bus.add(id) - done = true - } - } + @DisplayName("Calls to rules defined in ktlint 0.47 and after") + @Nested + inner class RuleExecutionCalls { + @Test + fun `Given a normal rule then execute on root node and child nodes`() { + val ruleExecutionCalls = mutableListOf() + KtLint.lint( + KtLint.ExperimentalParams( + // An empty file results in nodes with elementTypes FILE, PACKAGE_DIRECTIVE and IMPORT_LIST respectively + text = "", + ruleSets = listOf( + RuleSet( + "standard", + SimpleTestRule( + ruleExecutionCalls = ruleExecutionCalls, + id = "a", + visitorModifiers = setOf() + ), + SimpleTestRule( + ruleExecutionCalls = ruleExecutionCalls, + id = "b", + visitorModifiers = setOf(Rule.VisitorModifier.RunAsLateAsPossible) + ) + ) + ), + cb = { _, _ -> } + ) + ) + assertThat(ruleExecutionCalls).containsExactly( + // File a + RuleExecutionCall(BEFORE_FIRST, null, "a", null), + RuleExecutionCall(BEFORE_CHILDREN, ROOT, "a", FILE), + RuleExecutionCall(BEFORE_CHILDREN, CHILD, "a", PACKAGE_DIRECTIVE), + RuleExecutionCall(AFTER_CHILDREN, CHILD, "a", PACKAGE_DIRECTIVE), + RuleExecutionCall(BEFORE_CHILDREN, CHILD, "a", IMPORT_LIST), + RuleExecutionCall(AFTER_CHILDREN, CHILD, "a", IMPORT_LIST), + RuleExecutionCall(AFTER_CHILDREN, ROOT, "a", FILE), + RuleExecutionCall(AFTER_LAST, null, "a", null), + // File b + RuleExecutionCall(BEFORE_FIRST, null, "b", null), + RuleExecutionCall(BEFORE_CHILDREN, ROOT, "b", FILE), + RuleExecutionCall(BEFORE_CHILDREN, CHILD, "b", PACKAGE_DIRECTIVE), + RuleExecutionCall(AFTER_CHILDREN, CHILD, "b", PACKAGE_DIRECTIVE), + RuleExecutionCall(BEFORE_CHILDREN, CHILD, "b", IMPORT_LIST), + RuleExecutionCall(AFTER_CHILDREN, CHILD, "b", IMPORT_LIST), + RuleExecutionCall(AFTER_CHILDREN, ROOT, "b", FILE), + RuleExecutionCall(AFTER_LAST, null, "b", null) + ) } - val bus = mutableListOf() - KtLint.lint( - KtLint.ExperimentalParams( - text = "fun main() {}", - ruleSets = listOf( - RuleSet( - "standard", - object : R( - bus = bus, - id = "e", - visitorModifiers = setOf(VisitorModifier.RunAsLateAsPossible) - ) {}, - object : R( - bus = bus, - id = "d", - visitorModifiers = setOf( - VisitorModifier.RunOnRootNodeOnly, - VisitorModifier.RunAsLateAsPossible + + @Test + fun `Given a run-on-root-node-only rule then execute on root node but not on child nodes`() { + val ruleExecutionCalls = mutableListOf() + KtLint.lint( + KtLint.ExperimentalParams( + text = "fun main() {}", + ruleSets = listOf( + RuleSet( + "standard", + SimpleTestRule( + ruleExecutionCalls = ruleExecutionCalls, + id = "a", + visitorModifiers = setOf(RunOnRootNodeOnly) + ), + SimpleTestRule( + ruleExecutionCalls = ruleExecutionCalls, + id = "b", + visitorModifiers = setOf(RunOnRootNodeOnly, RunAsLateAsPossible) ) - ) {}, - R( - bus = bus, - id = "b" - ), - object : R( - bus = bus, - id = "a", - visitorModifiers = setOf( - VisitorModifier.RunOnRootNodeOnly + ) + ), + cb = { _, _ -> } + ) + ) + assertThat(ruleExecutionCalls).containsExactly( + // File a + RuleExecutionCall(BEFORE_FIRST, null, "a", null), + RuleExecutionCall(BEFORE_CHILDREN, ROOT, "a", FILE), + RuleExecutionCall(AFTER_CHILDREN, ROOT, "a", FILE), + RuleExecutionCall(AFTER_LAST, null, "a", null), + // File b + RuleExecutionCall(BEFORE_FIRST, null, "b", null), + RuleExecutionCall(BEFORE_CHILDREN, ROOT, "b", FILE), + RuleExecutionCall(AFTER_CHILDREN, ROOT, "b", FILE), + RuleExecutionCall(AFTER_LAST, null, "b", null) + ) + } + + @Test + fun `Given multiple rules which have to run in a certain order`() { + val ruleExecutionCalls = mutableListOf() + KtLint.lint( + KtLint.ExperimentalParams( + // An empty file results in nodes with elementTypes FILE, PACKAGE_DIRECTIVE and IMPORT_LIST respectively + text = "", + ruleSets = listOf( + RuleSet( + "standard", + SimpleTestRule( + ruleExecutionCalls = ruleExecutionCalls, + id = "e", + visitorModifiers = setOf(RunAsLateAsPossible) + ), + SimpleTestRule( + ruleExecutionCalls = ruleExecutionCalls, + id = "d", + visitorModifiers = setOf( + RunOnRootNodeOnly, + RunAsLateAsPossible + ) + ), + SimpleTestRule( + ruleExecutionCalls = ruleExecutionCalls, + id = "b" + ), + SimpleTestRule( + ruleExecutionCalls = ruleExecutionCalls, + id = "a", + visitorModifiers = setOf( + RunOnRootNodeOnly + ) + ), + SimpleTestRule( + ruleExecutionCalls = ruleExecutionCalls, + id = "c" ) - ) {}, - R( - bus = bus, - id = "c" ) - ) - ), - cb = { _, _ -> } + ), + cb = { _, _ -> } + ) ) - ) - assertThat(bus).isEqualTo(listOf("file:a", "file:b", "file:c", "file:d", "file:e", "b", "c", "e")) + assertThat(ruleExecutionCalls).containsExactly( + // File a (root only) + RuleExecutionCall(BEFORE_FIRST, null, "a", null), + RuleExecutionCall(BEFORE_CHILDREN, ROOT, "a", FILE), + RuleExecutionCall(AFTER_CHILDREN, ROOT, "a", FILE), + RuleExecutionCall(AFTER_LAST, null, "a", null), + // File b + RuleExecutionCall(BEFORE_FIRST, null, "b", null), + RuleExecutionCall(BEFORE_CHILDREN, ROOT, "b", FILE), + RuleExecutionCall(BEFORE_CHILDREN, CHILD, "b", PACKAGE_DIRECTIVE), + RuleExecutionCall(AFTER_CHILDREN, CHILD, "b", PACKAGE_DIRECTIVE), + RuleExecutionCall(BEFORE_CHILDREN, CHILD, "b", IMPORT_LIST), + RuleExecutionCall(AFTER_CHILDREN, CHILD, "b", IMPORT_LIST), + RuleExecutionCall(AFTER_CHILDREN, ROOT, "b", FILE), + RuleExecutionCall(AFTER_LAST, null, "b", null), + // File c + RuleExecutionCall(BEFORE_FIRST, null, "c", null), + RuleExecutionCall(BEFORE_CHILDREN, ROOT, "c", FILE), + RuleExecutionCall(BEFORE_CHILDREN, CHILD, "c", PACKAGE_DIRECTIVE), + RuleExecutionCall(AFTER_CHILDREN, CHILD, "c", PACKAGE_DIRECTIVE), + RuleExecutionCall(BEFORE_CHILDREN, CHILD, "c", IMPORT_LIST), + RuleExecutionCall(AFTER_CHILDREN, CHILD, "c", IMPORT_LIST), + RuleExecutionCall(AFTER_CHILDREN, ROOT, "c", FILE), + RuleExecutionCall(AFTER_LAST, null, "c", null), + // File d (root only) + RuleExecutionCall(BEFORE_FIRST, null, "d", null), + RuleExecutionCall(BEFORE_CHILDREN, ROOT, "d", FILE), + RuleExecutionCall(AFTER_CHILDREN, ROOT, "d", FILE), + RuleExecutionCall(AFTER_LAST, null, "d", null), + // File e + RuleExecutionCall(BEFORE_FIRST, null, "e", null), + RuleExecutionCall(BEFORE_CHILDREN, ROOT, "e", FILE), + RuleExecutionCall(BEFORE_CHILDREN, CHILD, "e", PACKAGE_DIRECTIVE), + RuleExecutionCall(AFTER_CHILDREN, CHILD, "e", PACKAGE_DIRECTIVE), + RuleExecutionCall(BEFORE_CHILDREN, CHILD, "e", IMPORT_LIST), + RuleExecutionCall(AFTER_CHILDREN, CHILD, "e", IMPORT_LIST), + RuleExecutionCall(AFTER_CHILDREN, ROOT, "e", FILE), + RuleExecutionCall(AFTER_LAST, null, "e", null) + ) + } } @Test @@ -479,13 +733,6 @@ private class DummyRuleWithCustomEditorConfigProperty : override val editorConfigProperties: List> = listOf(someCustomRuleProperty) - override fun visit( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit - ) { - } - companion object { const val SOME_CUSTOM_RULE_PROPERTY = "some-custom-rule-property" @@ -508,7 +755,7 @@ private class DummyRuleWithCustomEditorConfigProperty : private open class DummyRule( val block: (node: ASTNode) -> Unit = {} ) : Rule("dummy-rule") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit @@ -549,6 +796,85 @@ private class AutoCorrectErrorRule : Rule("auto-correct") { } } +/** + * Rule in style up to ktlint 0.46.x in which a rule only has to override method [Rule.beforeVisitChildNodes]. For each invocation to + * this method a [RuleExecutionCall] is added to the list of previously calls made. + */ +private class SimpleTestRuleLegacy( + private val ruleExecutionCalls: MutableList, + id: String, + visitorModifiers: Set = emptySet() +) : Rule(id, visitorModifiers) { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + ruleExecutionCalls.add(RuleExecutionCall(VISIT, node.visitNodeType, id, node.elementType)) + } +} + +/** + * Rule in style starting from ktlint 0.47.x in which a rule can can override method [Rule.beforeFirstNode], + * [Rule.beforeVisitChildNodes], [Rule.afterVisitChildNodes] and [Rule.afterLastNode]. For each invocation to + * this method a [RuleExecutionCall] is added to the list of previously calls made. + */ +private class SimpleTestRule( + private val ruleExecutionCalls: MutableList, + id: String, + visitorModifiers: Set = emptySet() +) : Rule(id, visitorModifiers) { + override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) { + ruleExecutionCalls.add(RuleExecutionCall(BEFORE_FIRST, null, id, null)) + } + + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + ruleExecutionCalls.add(RuleExecutionCall(BEFORE_CHILDREN, node.visitNodeType, id, node.elementType)) + } + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + ruleExecutionCalls.add(RuleExecutionCall(VISIT, node.visitNodeType, id, node.elementType)) + } + + override fun afterVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + ruleExecutionCalls.add(RuleExecutionCall(AFTER_CHILDREN, node.visitNodeType, id, node.elementType)) + } + + override fun afterLastNode() { + ruleExecutionCalls.add(RuleExecutionCall(AFTER_LAST, null, id, null)) + } +} + +private data class RuleExecutionCall( + val ruleMethod: RuleMethod, + val visitNodeType: VisitNodeType?, + val id: String, + val file: IElementType? +) { + enum class RuleMethod { BEFORE_FIRST, BEFORE_CHILDREN, VISIT, AFTER_CHILDREN, AFTER_LAST } + enum class VisitNodeType { ROOT, CHILD } +} + +private val ASTNode.visitNodeType: RuleExecutionCall.VisitNodeType + get() = + if (isRoot()) { + ROOT + } else { + CHILD + } + private fun getResourceAsText(path: String) = (ClassLoader.getSystemClassLoader().getResourceAsStream(path) ?: throw RuntimeException("$path not found")) .bufferedReader() diff --git a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/UsesEditorConfigPropertiesTest.kt b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/UsesEditorConfigPropertiesTest.kt index 986389c0c4..704ee627ba 100644 --- a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/UsesEditorConfigPropertiesTest.kt +++ b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/UsesEditorConfigPropertiesTest.kt @@ -6,175 +6,172 @@ import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.maxLineLength import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import org.assertj.core.api.Assertions.assertThat import org.ec4j.core.model.Property -import org.jetbrains.kotlin.com.intellij.lang.ASTNode -import org.jetbrains.kotlin.com.intellij.psi.impl.source.DummyHolderElement +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test class UsesEditorConfigPropertiesTest { - class PropertyValueTester(editorConfigProperty: UsesEditorConfigProperties.EditorConfigProperty) : UsesEditorConfigProperties { - override val editorConfigProperties: List> = listOf(editorConfigProperty) - - fun testValue( - node: ASTNode, - editorConfigProperty: UsesEditorConfigProperties.EditorConfigProperty - ): T = node.getEditorConfigValue(editorConfigProperty) - } - - @Test - fun `Given that editor config property indent_size is set to an integer value then return that integer value via the getEditorConfigValue of the node`() { - val testAstNode: ASTNode = DummyHolderElement("some-text") - testAstNode.putUserData( - KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, - createPropertyWithValue( + @Nested + inner class IndentSizeProperty { + @Test + fun `Given that editor config property indent_size is set to an integer value then return that integer value via the getEditorConfigValue of the node`() { + val editorConfigProperties = createEditorConfigPropertiesFrom( indentSizeProperty, SOME_INTEGER_VALUE.toString() ) - ) - val actual = PropertyValueTester(indentSizeProperty).testValue(testAstNode, indentSizeProperty) - assertThat(actual).isEqualTo(SOME_INTEGER_VALUE) - } + val actual = with(EditorConfigPropertiesTester(indentSizeProperty)) { + editorConfigProperties.getEditorConfigValue(indentSizeProperty) + } + + assertThat(actual).isEqualTo(SOME_INTEGER_VALUE) + } - @Test - fun `Given that editor config property indent_size is set to value 'unset' then return -1 as value via the getEditorConfigValue of the node`() { - val testAstNode: ASTNode = DummyHolderElement("some-text") - testAstNode.putUserData( - KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, - createPropertyWithValue( + @Test + fun `Given that editor config property indent_size is set to value 'unset' then return -1 as value via the getEditorConfigValue of the node`() { + val editorConfigProperties = createEditorConfigPropertiesFrom( indentSizeProperty, "unset" ) - ) - val actual = PropertyValueTester(indentSizeProperty).testValue(testAstNode, indentSizeProperty) - assertThat(actual).isEqualTo(-1) - } + val actual = with(EditorConfigPropertiesTester(indentSizeProperty)) { + editorConfigProperties.getEditorConfigValue(indentSizeProperty) + } + + assertThat(actual).isEqualTo(-1) + } - @Test - fun `Issue 1485 - Given that editor config property indent_size is set to value 'tab' then return tabWidth as value via the getEditorConfigValue of the node`() { - val testAstNode: ASTNode = DummyHolderElement("some-text") - testAstNode.putUserData( - KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, - createPropertyWithValue( + @Test + fun `Issue 1485 - Given that editor config property indent_size is set to value 'tab' then return tabWidth as value via the getEditorConfigValue of the node`() { + val editorConfigProperties = createEditorConfigPropertiesFrom( indentSizeProperty, "tab" ) - ) - val actual = PropertyValueTester(indentSizeProperty).testValue(testAstNode, indentSizeProperty) - assertThat(actual).isEqualTo(IndentConfig.DEFAULT_INDENT_CONFIG.tabWidth) - } + val actual = with(EditorConfigPropertiesTester(indentSizeProperty)) { + editorConfigProperties.getEditorConfigValue(indentSizeProperty) + } - @Test - fun `Given that editor config property indent_size is not set then return the default tabWidth as value via the getEditorConfigValue of the node`() { - val testAstNode: ASTNode = DummyHolderElement("some-text") - testAstNode.putUserData( - KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, - emptyMap() - ) - val actual = PropertyValueTester(indentSizeProperty).testValue(testAstNode, indentSizeProperty) + assertThat(actual).isEqualTo(IndentConfig.DEFAULT_INDENT_CONFIG.tabWidth) + } + + @Test + fun `Given that editor config property indent_size is not set then return the default tabWidth as value via the getEditorConfigValue of the node`() { + val actual = with(EditorConfigPropertiesTester(indentSizeProperty)) { + emptyMap().getEditorConfigValue(indentSizeProperty) + } + + assertThat(actual).isEqualTo(IndentConfig.DEFAULT_INDENT_CONFIG.tabWidth) - assertThat(actual).isEqualTo(IndentConfig.DEFAULT_INDENT_CONFIG.tabWidth) + assertThat(actual).isEqualTo(IndentConfig.DEFAULT_INDENT_CONFIG.tabWidth) + } } - @Test - fun `Given that editor config property max_line_length is set to an integer value then return that integer value via the getEditorConfigValue of the node`() { - val testAstNode: ASTNode = DummyHolderElement("some-text") - testAstNode.putUserData( - KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, - createPropertyWithValue( + @Nested + inner class MaxLineLengthProperty { + @Test + fun `Given that editor config property max_line_length is set to an integer value then return that integer value via the getEditorConfigValue of the node`() { + val editorConfigProperties = createEditorConfigPropertiesFrom( maxLineLengthProperty, SOME_INTEGER_VALUE.toString() ) - ) - val actual = PropertyValueTester(maxLineLengthProperty).testValue(testAstNode, maxLineLengthProperty) - assertThat(actual).isEqualTo(SOME_INTEGER_VALUE) - } + val actual = with(EditorConfigPropertiesTester(maxLineLengthProperty)) { + editorConfigProperties.getEditorConfigValue(maxLineLengthProperty) + } - @Test - fun `Given that editor config property max_line_length is set to value 'off' then return -1 via the getEditorConfigValue of the node`() { - val testAstNode: ASTNode = DummyHolderElement("some-text") - testAstNode.putUserData( - KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, - createPropertyWithValue( + assertThat(actual).isEqualTo(SOME_INTEGER_VALUE) + } + + @Test + fun `Given that editor config property max_line_length is set to value 'off' then return -1 via the getEditorConfigValue of the node`() { + val editorConfigProperties = createEditorConfigPropertiesFrom( maxLineLengthProperty, "off" ) - ) - val actual = PropertyValueTester(maxLineLengthProperty).testValue(testAstNode, maxLineLengthProperty) - assertThat(actual).isEqualTo(-1) - } + val actual = with(EditorConfigPropertiesTester(maxLineLengthProperty)) { + editorConfigProperties.getEditorConfigValue(maxLineLengthProperty) + } - @Test - fun `Given that editor config property max_line_length is set to value 'unset' for android then return 100 via the getEditorConfigValue of the node`() { - val testAstNode: ASTNode = DummyHolderElement("some-text") - testAstNode.putUserData( - KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, - createPropertyWithValue(maxLineLengthProperty, "unset").plus(ANDROID_CODE_STYLE) - ) - val actual = PropertyValueTester(maxLineLengthProperty).testValue(testAstNode, maxLineLengthProperty) + assertThat(actual).isEqualTo(-1) + } - assertThat(actual).isEqualTo(100) - } + @Test + fun `Given that editor config property max_line_length is set to value 'unset' for android then return 100 via the getEditorConfigValue of the node`() { + val editorConfigProperties = createEditorConfigPropertiesFrom( + maxLineLengthProperty, + "unset" + ).plus(ANDROID_CODE_STYLE) - @Test - fun `Given that editor config property max_line_length is set to value 'unset' for non-android then return -1 via the getEditorConfigValue of the node`() { - val testAstNode: ASTNode = DummyHolderElement("some-text") - testAstNode.putUserData( - KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, - createPropertyWithValue(maxLineLengthProperty, "unset").plus(OFFICIAL_CODE_STYLE) - ) - val actual = PropertyValueTester(maxLineLengthProperty).testValue(testAstNode, maxLineLengthProperty) + val actual = with(EditorConfigPropertiesTester(maxLineLengthProperty)) { + editorConfigProperties.getEditorConfigValue(maxLineLengthProperty) + } - assertThat(actual).isEqualTo(-1) - } + assertThat(actual).isEqualTo(100) + } - @Test - fun `Given that editor config property max_line_length is not set for android then return 100 via the getEditorConfigValue of the node`() { - val testAstNode: ASTNode = DummyHolderElement("some-text") - testAstNode.putUserData( - KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, - ANDROID_CODE_STYLE - ) - val actual = PropertyValueTester(maxLineLengthProperty).testValue(testAstNode, maxLineLengthProperty) + @Test + fun `Given that editor config property max_line_length is set to value 'unset' for non-android then return -1 via the getEditorConfigValue of the node`() { + val editorConfigProperties = createEditorConfigPropertiesFrom( + maxLineLengthProperty, + "unset" + ).plus(OFFICIAL_CODE_STYLE) - assertThat(actual).isEqualTo(100) - } + val actual = with(EditorConfigPropertiesTester(maxLineLengthProperty)) { + editorConfigProperties.getEditorConfigValue(maxLineLengthProperty) + } - @Test - fun `Given that editor config property max_line_length is not set for non-android then return -1 via the getEditorConfigValue of the node`() { - val testAstNode: ASTNode = DummyHolderElement("some-text") - testAstNode.putUserData( - KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, - OFFICIAL_CODE_STYLE - ) - val actual = PropertyValueTester(maxLineLengthProperty).testValue(testAstNode, maxLineLengthProperty) + assertThat(actual).isEqualTo(-1) + } + + @Test + fun `Given that editor config property max_line_length is not set for android then return 100 via the getEditorConfigValue of the node`() { + val actual = with(EditorConfigPropertiesTester(maxLineLengthProperty)) { + ANDROID_CODE_STYLE.getEditorConfigValue(maxLineLengthProperty) + } + + assertThat(actual).isEqualTo(100) + } + + @Test + fun `Given that editor config property max_line_length is not set for non-android then return -1 via the getEditorConfigValue of the node`() { + val actual = with(EditorConfigPropertiesTester(maxLineLengthProperty)) { + OFFICIAL_CODE_STYLE.getEditorConfigValue(maxLineLengthProperty) + } - assertThat(actual).isEqualTo(-1) + assertThat(actual).isEqualTo(-1) + } + } + + class EditorConfigPropertiesTester( + editorConfigProperty: UsesEditorConfigProperties.EditorConfigProperty + ) : UsesEditorConfigProperties { + override val editorConfigProperties: List> = listOf(editorConfigProperty) } private companion object { const val SOME_INTEGER_VALUE = 123 - val ANDROID_CODE_STYLE = createPropertyWithValue( + val ANDROID_CODE_STYLE = createEditorConfigPropertiesFrom( DefaultEditorConfigProperties.codeStyleSetProperty, DefaultEditorConfigProperties.CodeStyleValue.android.name.lowercase() ) - val OFFICIAL_CODE_STYLE = createPropertyWithValue( + val OFFICIAL_CODE_STYLE = createEditorConfigPropertiesFrom( DefaultEditorConfigProperties.codeStyleSetProperty, DefaultEditorConfigProperties.CodeStyleValue.official.name.lowercase() ) - private fun createPropertyWithValue( + private fun createEditorConfigPropertiesFrom( editorConfigProperty: UsesEditorConfigProperties.EditorConfigProperty, value: String - ) = mapOf( - editorConfigProperty.type.name to Property.builder() - .name(editorConfigProperty.type.name) - .type(editorConfigProperty.type) - .value(value) - .build() - ) + ) = + with(editorConfigProperty) { + mapOf( + type.name to Property.builder() + .name(type.name) + .type(type) + .value(value) + .build() + ) + } } } diff --git a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/VisitorProviderTest.kt b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/VisitorProviderTest.kt index 6070fcb005..59cbb2e37e 100644 --- a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/VisitorProviderTest.kt +++ b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/VisitorProviderTest.kt @@ -1,34 +1,13 @@ package com.pinterest.ktlint.core import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.disabledRulesProperty -import com.pinterest.ktlint.core.api.EditorConfigOverride -import com.pinterest.ktlint.core.ast.ElementType.FILE -import com.pinterest.ktlint.core.ast.ElementType.IMPORT_LIST -import com.pinterest.ktlint.core.ast.ElementType.PACKAGE_DIRECTIVE import com.pinterest.ktlint.core.internal.VisitorProvider -import com.pinterest.ktlint.core.internal.initPsiFileFactory import org.assertj.core.api.Assertions.assertThat import org.ec4j.core.model.Property import org.jetbrains.kotlin.com.intellij.lang.ASTNode -import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType -import org.jetbrains.kotlin.idea.KotlinLanguage -import org.jetbrains.kotlin.psi.KtFile import org.junit.jupiter.api.Test class VisitorProviderTest { - @Test - fun `A normal rule visits all nodes`() { - val actual = testVisitorProvider( - NormalRule(NORMAL_RULE) - ) - - assertThat(actual).containsExactly( - Visit(NORMAL_RULE, FILE), - Visit(NORMAL_RULE, PACKAGE_DIRECTIVE), - Visit(NORMAL_RULE, IMPORT_LIST) - ) - } - @Test fun `A root only rule only visits the FILE node only`() { val actual = testVisitorProvider( @@ -41,15 +20,17 @@ class VisitorProviderTest { } @Test - fun `A run as late as possible rule visits all nodes`() { + fun `A run-as-late-as-possible-rule runs later than normal rules`() { val actual = testVisitorProvider( - RunAsLateAsPossibleRule(RUN_AS_LATE_AS_POSSIBLE_RULE) + NormalRule(RULE_A), + RunAsLateAsPossibleRule(RULE_B), + NormalRule(RULE_C) ) assertThat(actual).containsExactly( - Visit(RUN_AS_LATE_AS_POSSIBLE_RULE, FILE), - Visit(RUN_AS_LATE_AS_POSSIBLE_RULE, PACKAGE_DIRECTIVE), - Visit(RUN_AS_LATE_AS_POSSIBLE_RULE, IMPORT_LIST) + Visit(RULE_A), + Visit(RULE_C), + Visit(RULE_B) ) } @@ -60,7 +41,7 @@ class VisitorProviderTest { ) assertThat(actual).containsExactly( - Visit(RUN_AS_LATE_AS_POSSIBLE_RULE, FILE) + Visit(RUN_AS_LATE_AS_POSSIBLE_RULE) ) } @@ -82,7 +63,7 @@ class VisitorProviderTest { NormalRule(RULE_C), NormalRule(SOME_DISABLED_RULE_IN_CUSTOM_RULE_SET_A) ) - ).filterFileNodes() + ) assertThat(actual).containsExactly( Visit(RULE_A), @@ -119,42 +100,19 @@ class VisitorProviderTest { assertThat(actual).isNull() } - @Test - fun `Visits all rules on a node concurrently before proceeding to the next node`() { - val actual = testVisitorProvider( - NormalRule(RULE_A), - NormalRule(RULE_B), - concurrent = true - ) - - assertThat(actual).containsExactly( - Visit(RULE_A, FILE), - Visit(RULE_B, FILE), - Visit(RULE_A, PACKAGE_DIRECTIVE), - Visit(RULE_B, PACKAGE_DIRECTIVE), - Visit(RULE_A, IMPORT_LIST), - Visit(RULE_B, IMPORT_LIST) - ) - } - /** * Create a visitor provider for a given list of rules in the same rule set (STANDARD). It returns a list of visits * that the provider made after it was invoked. The tests of the visitor provider should only focus on whether the * visit provider has invoked the correct rules in the correct order. Note that the testProvider does not invoke the * real visit method of the rule. */ - private fun testVisitorProvider( - vararg rules: Rule, - concurrent: Boolean? = null - ): MutableList? { - return testVisitorProvider( + private fun testVisitorProvider(vararg rules: Rule): MutableList? = + testVisitorProvider( RuleSet( STANDARD, *rules - ), - concurrent = concurrent + ) ) - } /** * Create a visitor provider for a given list of rule sets. It returns a list of visits that the provider made @@ -162,24 +120,13 @@ class VisitorProviderTest { * invoked the correct rules in the correct order. Note that the testProvider does not invoke the real visit method * of the rule. */ - private fun testVisitorProvider( - vararg ruleSets: RuleSet, - concurrent: Boolean? = null - ): MutableList? { + private fun testVisitorProvider(vararg ruleSets: RuleSet): MutableList? { val ruleSetList = ruleSets.toList() return VisitorProvider( params = KtLint.ExperimentalParams( text = "", cb = { _, _ -> }, ruleSets = ruleSetList, - editorConfigOverride = EditorConfigOverride.from( - disabledRulesProperty to - listOf( - SOME_DISABLED_RULE_IN_STANDARD_RULE_SET, - "$EXPERIMENTAL:$SOME_DISABLED_RULE_IN_EXPERIMENTAL_RULE_SET", - "$CUSTOM_RULE_SET_A:$SOME_DISABLED_RULE_IN_CUSTOM_RULE_SET_A" - ).joinToString(separator = ",") - ), // Enable debug mode as it is helpful when a test fails debug = true ), @@ -188,30 +135,38 @@ class VisitorProviderTest { recreateRuleSorter = true ).run { var visits: MutableList? = null - visitor(SOME_ROOT_AST_NODE, concurrent ?: false) - .invoke { node, _, fqRuleId -> - if (visits == null) { - visits = mutableListOf() - } - visits?.add(Visit(fqRuleId, node.elementType)) + visitor( + disabledRulesEditorConfigProperties( + SOME_DISABLED_RULE_IN_STANDARD_RULE_SET, + "$EXPERIMENTAL:$SOME_DISABLED_RULE_IN_EXPERIMENTAL_RULE_SET", + "$CUSTOM_RULE_SET_A:$SOME_DISABLED_RULE_IN_CUSTOM_RULE_SET_A" + ) + ).invoke { _, fqRuleId -> + if (visits == null) { + visits = mutableListOf() } + visits?.add(Visit(fqRuleId)) + } visits } } - /** - * When visiting a node with a normal rule, this results in multiple visits. In most tests this would bloat the - * assertion needlessly. - */ - private fun List?.filterFileNodes(): List? = - this?.filter { it.elementType == FILE } + private fun disabledRulesEditorConfigProperties(vararg ruleIds: String) = + with(disabledRulesProperty) { + mapOf( + type.name to + Property.builder() + .name(type.name) + .type(type) + .value(ruleIds.joinToString(separator = ",")) + .build() + ) + } private companion object { const val STANDARD = "standard" const val EXPERIMENTAL = "experimental" const val CUSTOM_RULE_SET_A = "custom-rule-set-a" - val SOME_ROOT_AST_NODE = initRootAstNode() - const val NORMAL_RULE = "normal-rule" const val ROOT_NODE_ONLY_RULE = "root-node-only-rule" const val RUN_AS_LATE_AS_POSSIBLE_RULE = "run-as-late-as-possible-rule" const val RULE_A = "rule-a" @@ -221,43 +176,6 @@ class VisitorProviderTest { const val SOME_DISABLED_RULE_IN_EXPERIMENTAL_RULE_SET = "some-disabled-rule-in-experimental-rule-set" const val SOME_DISABLED_RULE_IN_CUSTOM_RULE_SET_A = "some-disabled-rule-custom-rule-set-a" const val COMMA_FOLLOWED_BY_SPACE_SEPARATOR = ", " - - fun initRootAstNode(): ASTNode { - initPsiFileFactory(false).apply { - val psiFile = createFileFromText( - "unit-test.kt", - KotlinLanguage.INSTANCE, - // An empty file results in three ASTNodes which are all empty: - // - kotlin.FILE (root node) - // - PACKAGE_DIRECTIVE - // - IMPORT_LIST - "" - ) as KtFile - return psiFile.node.apply { - putUserData( - KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, - mapOf( - "disabled_rules" to - Property.builder() - .name("disabled_rules") - .type(disabledRulesProperty.type) - .value( - // When IntelliJ IDEA is reformatting the ".editorconfig" file it sometimes add - // a space after the comma in a comma-separate-list. Below such an unexpected - // property value is build to ensure that it is handled properly. - buildString { - append("$EXPERIMENTAL:$SOME_DISABLED_RULE_IN_EXPERIMENTAL_RULE_SET") - append(COMMA_FOLLOWED_BY_SPACE_SEPARATOR) - append("$CUSTOM_RULE_SET_A:$SOME_DISABLED_RULE_IN_CUSTOM_RULE_SET_A") - append(COMMA_FOLLOWED_BY_SPACE_SEPARATOR) - append(SOME_DISABLED_RULE_IN_STANDARD_RULE_SET) - } - ).build() - ) - ) - } - } - } } open class NormalRule(id: String) : R(id) @@ -290,7 +208,7 @@ class VisitorProviderTest { ) : Rule(id, visitorModifiers) { constructor(id: String, visitorModifier: VisitorModifier) : this(id, setOf(visitorModifier)) - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit @@ -301,17 +219,12 @@ class VisitorProviderTest { } } - private data class Visit( - val shortenedQualifiedRuleId: String, - val elementType: IElementType = FILE - ) { + private data class Visit(val shortenedQualifiedRuleId: String) { constructor( ruleSetId: String, - ruleId: String, - elementType: IElementType = FILE + ruleId: String ) : this( - shortenedQualifiedRuleId = "$ruleSetId:$ruleId", - elementType = elementType + shortenedQualifiedRuleId = "$ruleSetId:$ruleId" ) { require(!ruleSetId.contains(':')) { "rule set id may not contain the ':' character" diff --git a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigGeneratorTest.kt b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigGeneratorTest.kt index 0e8f2a8ecc..8a56ddda6c 100644 --- a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigGeneratorTest.kt +++ b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigGeneratorTest.kt @@ -206,7 +206,7 @@ internal class EditorConfigGeneratorTest { } private open class TestRule(ruleId: String) : Rule(ruleId) { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoaderTest.kt b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoaderTest.kt index a87c1271db..b9ac707d2c 100644 --- a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoaderTest.kt +++ b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoaderTest.kt @@ -482,7 +482,7 @@ internal class EditorConfigLoaderTest { private class TestRule : Rule("editorconfig-test"), UsesEditorConfigProperties { override val editorConfigProperties: List> = emptyList() - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/RuleSorterTest.kt b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/RuleSorterTest.kt index a5987191c8..04de49bf19 100644 --- a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/RuleSorterTest.kt +++ b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/RuleSorterTest.kt @@ -541,7 +541,7 @@ class RuleSorterTest { ) : Rule(id, visitorModifiers) { constructor(id: String, visitorModifier: VisitorModifier) : this(id, setOf(visitorModifier)) - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/SuppressionLocatorBuilderTest.kt b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/SuppressionLocatorBuilderTest.kt index 2f87249a6a..8352e83b30 100644 --- a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/SuppressionLocatorBuilderTest.kt +++ b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/SuppressionLocatorBuilderTest.kt @@ -204,7 +204,7 @@ class SuppressionLocatorBuilderTest { } private class NoFooIdentifierRule(id: String) : Rule(id) { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/BlockCommentInitialStarAlignmentRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/BlockCommentInitialStarAlignmentRule.kt index 5212459140..d2f0f5aa34 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/BlockCommentInitialStarAlignmentRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/BlockCommentInitialStarAlignmentRule.kt @@ -19,7 +19,7 @@ class BlockCommentInitialStarAlignmentRule : VisitorModifier.RunAfterRule("standard:indent") ) ) { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/CommentWrappingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/CommentWrappingRule.kt index ae527cf705..22fdd65f8f 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/CommentWrappingRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/CommentWrappingRule.kt @@ -28,7 +28,7 @@ public class CommentWrappingRule : DefaultEditorConfigProperties.indentStyleProperty ) - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/DiscouragedCommentLocationRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/DiscouragedCommentLocationRule.kt index 9f8a4053b1..401fe93847 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/DiscouragedCommentLocationRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/DiscouragedCommentLocationRule.kt @@ -28,7 +28,7 @@ import org.jetbrains.kotlin.com.intellij.lang.ASTNode * ``` */ public class DiscouragedCommentLocationRule : Rule("$experimentalRulesetId:discouraged-comment-location") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunKeywordSpacingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunKeywordSpacingRule.kt index c62ba33fa3..f25a4af4a7 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunKeywordSpacingRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunKeywordSpacingRule.kt @@ -11,7 +11,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement * Lints and formats the spacing after the fun keyword */ public class FunKeywordSpacingRule : Rule("$experimentalRulesetId:fun-keyword-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionReturnTypeSpacingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionReturnTypeSpacingRule.kt index 2518f99a53..6ed3f96606 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionReturnTypeSpacingRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionReturnTypeSpacingRule.kt @@ -11,7 +11,7 @@ import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement public class FunctionReturnTypeSpacingRule : Rule("$experimentalRulesetId:function-return-type-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionSignatureRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionSignatureRule.kt index 6e0b624ec5..50b3a2b590 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionSignatureRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionSignatureRule.kt @@ -5,6 +5,7 @@ import com.pinterest.ktlint.core.Rule import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.indentSizeProperty import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.indentStyleProperty import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.maxLineLengthProperty +import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION_ENTRY import com.pinterest.ktlint.core.ast.ElementType.BLOCK @@ -20,7 +21,6 @@ import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER_LIST import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE import com.pinterest.ktlint.core.ast.children -import com.pinterest.ktlint.core.ast.isRoot import com.pinterest.ktlint.core.ast.isWhiteSpace import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline import com.pinterest.ktlint.core.ast.lineIndent @@ -66,26 +66,24 @@ public class FunctionSignatureRule : private var functionSignatureWrappingMinimumParameters = -1 private var functionBodyExpressionWrapping = default - override fun visit( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit - ) { - if (node.isRoot()) { - functionSignatureWrappingMinimumParameters = node.getEditorConfigValue(forceMultilineWhenParameterCountGreaterOrEqualThanProperty) - functionBodyExpressionWrapping = node.getEditorConfigValue(functionBodyExpressionWrappingProperty) + override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) { + with(editorConfigProperties) { + functionSignatureWrappingMinimumParameters = getEditorConfigValue(forceMultilineWhenParameterCountGreaterOrEqualThanProperty) + functionBodyExpressionWrapping = getEditorConfigValue(functionBodyExpressionWrappingProperty) val indentConfig = IndentConfig( - indentStyle = node.getEditorConfigValue(indentStyleProperty), - tabWidth = node.getEditorConfigValue(indentSizeProperty) + indentStyle = getEditorConfigValue(indentStyleProperty), + tabWidth = getEditorConfigValue(indentSizeProperty) ) - if (indentConfig.disabled) { - return - } indent = indentConfig.indent - maxLineLength = node.getEditorConfigValue(maxLineLengthProperty) - return + maxLineLength = getEditorConfigValue(maxLineLengthProperty) } + } + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { if (node.elementType == FUN) { node .functionSignatureNodes() diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionStartOfBodySpacingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionStartOfBodySpacingRule.kt index afd8871eab..683b23ad75 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionStartOfBodySpacingRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionStartOfBodySpacingRule.kt @@ -15,7 +15,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement * Lints and formats the spacing after the fun keyword */ public class FunctionStartOfBodySpacingRule : Rule("$experimentalRulesetId:function-start-of-body-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionTypeReferenceSpacingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionTypeReferenceSpacingRule.kt index 6aa8656b8d..0f7f831668 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionTypeReferenceSpacingRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionTypeReferenceSpacingRule.kt @@ -10,7 +10,7 @@ import com.pinterest.ktlint.core.ast.nextSibling import org.jetbrains.kotlin.com.intellij.lang.ASTNode public class FunctionTypeReferenceSpacingRule : Rule("$experimentalRulesetId:function-type-reference-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/KdocWrappingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/KdocWrappingRule.kt index f373ff2ba8..8df1218a59 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/KdocWrappingRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/KdocWrappingRule.kt @@ -27,7 +27,7 @@ public class KdocWrappingRule : DefaultEditorConfigProperties.indentStyleProperty ) - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ModifierListSpacingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ModifierListSpacingRule.kt index cedf3d124d..6ebd17ff22 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ModifierListSpacingRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ModifierListSpacingRule.kt @@ -18,7 +18,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement * Lint and format the spacing between the modifiers in and after the last modifier in a modifier list. */ public class ModifierListSpacingRule : Rule("$experimentalRulesetId:modifier-list-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/NullableTypeSpacingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/NullableTypeSpacingRule.kt index 2a460d649d..7731e0912b 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/NullableTypeSpacingRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/NullableTypeSpacingRule.kt @@ -8,7 +8,7 @@ import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement public class NullableTypeSpacingRule : Rule("$experimentalRulesetId:nullable-type-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ParameterListSpacingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ParameterListSpacingRule.kt index 1c5baba1e9..74883a144d 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ParameterListSpacingRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ParameterListSpacingRule.kt @@ -26,7 +26,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement * interfering of the parameter-list-wrapping rule. */ public class ParameterListSpacingRule : Rule("$experimentalRulesetId:parameter-list-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/SpacingBetweenFunctionNameAndOpeningParenthesisRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/SpacingBetweenFunctionNameAndOpeningParenthesisRule.kt index 6802b88072..fc1509a9f0 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/SpacingBetweenFunctionNameAndOpeningParenthesisRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/SpacingBetweenFunctionNameAndOpeningParenthesisRule.kt @@ -7,7 +7,7 @@ import com.pinterest.ktlint.core.ast.nextSibling import org.jetbrains.kotlin.com.intellij.lang.ASTNode public class SpacingBetweenFunctionNameAndOpeningParenthesisRule : Rule("$experimentalRulesetId:spacing-between-function-name-and-opening-parenthesis") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeArgumentListSpacingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeArgumentListSpacingRule.kt index 26d190102d..58a3700538 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeArgumentListSpacingRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeArgumentListSpacingRule.kt @@ -23,7 +23,7 @@ import org.jetbrains.kotlin.com.intellij.lang.ASTNode * Lints and formats the spacing before and after the angle brackets of a type argument list. */ public class TypeArgumentListSpacingRule : Rule("$experimentalRulesetId:type-argument-list-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeParameterListSpacingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeParameterListSpacingRule.kt index 2ebe62bf18..8da3b37063 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeParameterListSpacingRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeParameterListSpacingRule.kt @@ -25,7 +25,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement * Lints and formats the spacing before and after the angle brackets of a type parameter list. */ public class TypeParameterListSpacingRule : Rule("$experimentalRulesetId:type-parameter-list-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/UnnecessaryParenthesesBeforeTrailingLambdaRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/UnnecessaryParenthesesBeforeTrailingLambdaRule.kt index a5d71eb8ba..ba187c27e6 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/UnnecessaryParenthesesBeforeTrailingLambdaRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/UnnecessaryParenthesesBeforeTrailingLambdaRule.kt @@ -15,7 +15,7 @@ import org.jetbrains.kotlin.com.intellij.lang.ASTNode * Ensures there are no unnecessary parentheses before a trailing lambda. */ class UnnecessaryParenthesesBeforeTrailingLambdaRule : Rule("$experimentalRulesetId:unnecessary-parentheses-before-trailing-lambda") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/AnnotationRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/AnnotationRule.kt index 4f2349667d..b7bede74b8 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/AnnotationRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/AnnotationRule.kt @@ -45,7 +45,7 @@ class AnnotationRule : Rule("annotation") { "File annotations should be separated from file contents with a blank line" } - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/AnnotationSpacingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/AnnotationSpacingRule.kt index e9f0729574..5eda6f5311 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/AnnotationSpacingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/AnnotationSpacingRule.kt @@ -30,7 +30,7 @@ class AnnotationSpacingRule : Rule("annotation-spacing") { const val ERROR_MESSAGE = "Annotations should occur immediately before the annotated construct" } - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ArgumentListWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ArgumentListWrappingRule.kt index 242c6f29c9..0c8e39cfd9 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ArgumentListWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ArgumentListWrappingRule.kt @@ -5,28 +5,26 @@ import com.pinterest.ktlint.core.Rule import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.indentSizeProperty import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.indentStyleProperty import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.maxLineLengthProperty +import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import com.pinterest.ktlint.core.ast.ElementType import com.pinterest.ktlint.core.ast.ElementType.ELSE -import com.pinterest.ktlint.core.ast.children +import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT_LIST import com.pinterest.ktlint.core.ast.column import com.pinterest.ktlint.core.ast.isPartOfComment -import com.pinterest.ktlint.core.ast.isRoot import com.pinterest.ktlint.core.ast.isWhiteSpace import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline import com.pinterest.ktlint.core.ast.lineIndent import com.pinterest.ktlint.core.ast.prevLeaf -import com.pinterest.ktlint.core.ast.visit -import kotlin.math.max 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.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.psi.KtContainerNode import org.jetbrains.kotlin.psi.KtDoWhileExpression import org.jetbrains.kotlin.psi.KtIfExpression import org.jetbrains.kotlin.psi.KtWhileExpression +import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType /** @@ -38,7 +36,7 @@ import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType * - maxLineLength exceeded (and separating arguments with \n would actually help) * in addition, "(" and ")" must be on separates line if any of the arguments are (otherwise on the same) */ -class ArgumentListWrappingRule : +public class ArgumentListWrappingRule : Rule("argument-list-wrapping"), UsesEditorConfigProperties { private var editorConfigIndent = IndentConfig.DEFAULT_INDENT_CONFIG @@ -51,25 +49,41 @@ class ArgumentListWrappingRule : maxLineLengthProperty ) - override fun visit( + // Keep state of argument list nodes for which the argument needs to be wrapped + private val wrapArgumentLists = mutableMapOf() + + // TODO: Eliminate NodeState when it contains only one field? + private data class NodeState(val newIndentLevel: Int) + + override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) { + editorConfigIndent = IndentConfig( + indentStyle = editorConfigProperties.getEditorConfigValue(indentStyleProperty), + tabWidth = editorConfigProperties.getEditorConfigValue(indentSizeProperty) + ) + maxLineLength = editorConfigProperties.getEditorConfigValue(maxLineLengthProperty) + } + + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { - if (node.isRoot()) { - editorConfigIndent = IndentConfig( - indentStyle = node.getEditorConfigValue(indentStyleProperty), - tabWidth = node.getEditorConfigValue(indentSizeProperty) - ) - maxLineLength = node.getEditorConfigValue(maxLineLengthProperty) - return - } if (editorConfigIndent.disabled) { return } - if (node.elementType == ElementType.VALUE_ARGUMENT_LIST && - // skip when there are no arguments + if (node.elementType == VALUE_ARGUMENT_LIST) { + if (needToWrapArgumentList(node)) { + val newIndentLevel = node.getNewIndentLevel() + node + .children() + .forEach { child -> wrapArgumentInList(newIndentLevel, child, emit, autoCorrect) } + } + } + } + + private fun needToWrapArgumentList(node: ASTNode) = + if ( // skip when there are no arguments node.firstChildNode?.treeNext?.elementType != ElementType.RPAR && // skip lambda arguments node.treeParent?.elementType != ElementType.FUNCTION_LITERAL && @@ -80,137 +94,111 @@ class ArgumentListWrappingRule : // - at least one of the arguments is // - maxLineLength exceeded (and separating arguments with \n would actually help) // in addition, "(" and ")" must be on separates line if any of the arguments are (otherwise on the same) - val putArgumentsOnSeparateLines = - node.textContainsIgnoringLambda('\n') || - // max_line_length exceeded - maxLineLength > -1 && (node.column - 1 + node.textLength) > maxLineLength && !node.textContains('\n') - if (putArgumentsOnSeparateLines) { - val currentIndentLevel = editorConfigIndent.indentLevelFrom(node.lineIndent()) - val newIndentLevel = - when { - // IDEA quirk: - // generic< - // T, - // R>( - // 1, - // 2 - // ) - // instead of - // generic< - // T, - // R>( - // 1, - // 2 - // ) - currentIndentLevel > 0 && node.hasTypeArgumentListInFront() -> currentIndentLevel - 1 + node.textContainsIgnoringLambda('\n') || node.exceedsMaxLineLength() + } else { + false + } - // IDEA quirk: - // foo - // .bar = Baz( - // 1, - // 2 - // ) - // instead of - // foo - // .bar = Baz( - // 1, - // 2 - // ) - currentIndentLevel > 0 && node.isPartOfDotQualifiedAssignmentExpression() -> currentIndentLevel - 1 + private fun ASTNode.exceedsMaxLineLength() = + maxLineLength > -1 && (column - 1 + textLength) > maxLineLength && !textContains('\n') - else -> currentIndentLevel - }.let { - if (node.isOnSameLineAsControlFlowKeyword()) { - it + 1 - } else { - it - } - } - val indent = "\n" + editorConfigIndent.indent.repeat(newIndentLevel) - - nextChild@ for (child in node.children()) { - when (child.elementType) { - ElementType.LPAR -> { - val prevLeaf = child.prevLeaf() - if (prevLeaf is PsiWhiteSpace && prevLeaf.textContains('\n')) { - emit(child.startOffset, errorMessage(child), true) - if (autoCorrect) { - prevLeaf.delete() - } - } - } - ElementType.VALUE_ARGUMENT, - ElementType.RPAR -> { - var argumentInnerIndentAdjustment = 0 + private fun ASTNode.getNewIndentLevel(): Int { + val currentIndentLevel = editorConfigIndent.indentLevelFrom(lineIndent()) + return when { + // IDEA quirk: + // generic< + // T, + // R>( + // 1, + // 2 + // ) + // instead of + // generic< + // T, + // R>( + // 1, + // 2 + // ) + currentIndentLevel > 0 && hasTypeArgumentListInFront() -> currentIndentLevel - 1 - // aiming for - // ... LPAR - // VALUE_PARAMETER... - // RPAR - val intendedIndent = if (child.elementType == ElementType.VALUE_ARGUMENT) { - indent + editorConfigIndent.indent - } else { - indent - } + // IDEA quirk: + // foo + // .bar = Baz( + // 1, + // 2 + // ) + // instead of + // foo + // .bar = Baz( + // 1, + // 2 + // ) + currentIndentLevel > 0 && isPartOfDotQualifiedAssignmentExpression() -> currentIndentLevel - 1 - val prevLeaf = child.prevWhiteSpaceWithNewLine() ?: child.prevLeaf() - if (prevLeaf is PsiWhiteSpace) { - if (prevLeaf.getText().contains("\n")) { - // The current child is already wrapped to a new line. Checking and fixing the - // correct size of the indent is the responsibility of the IndentationRule. - continue@nextChild - } else { - // The current child needs to be wrapped to a newline. - emit(child.startOffset, errorMessage(child), true) - if (autoCorrect) { - // The indentation is purely based on the previous leaf only. Note that in - // autoCorrect mode the indent rule, if enabled, runs after this rule and - // determines the final indentation. But if the indent rule is disabled then the - // indent of this rule is kept. - argumentInnerIndentAdjustment = intendedIndent.length - prevLeaf.getTextLength() - (prevLeaf as LeafPsiElement).rawReplaceWithText(intendedIndent) - } - } - } else { - // Insert a new whitespace element in order to wrap the current child to a new line. - emit(child.startOffset, errorMessage(child), true) - if (autoCorrect) { - argumentInnerIndentAdjustment = intendedIndent.length - child.column - node.addChild(PsiWhiteSpaceImpl(intendedIndent), child) - } - } - if (argumentInnerIndentAdjustment != 0 && child.elementType == ElementType.VALUE_ARGUMENT) { - child.visit { n -> - if (n.elementType == ElementType.WHITE_SPACE && n.textContains('\n')) { - val isInCollectionOrFunctionLiteral = - n.treeParent?.elementType == ElementType.COLLECTION_LITERAL_EXPRESSION || n.treeParent?.elementType == ElementType.FUNCTION_LITERAL + else -> currentIndentLevel + }.let { + if (isOnSameLineAsControlFlowKeyword()) { + it + 1 + } else { + it + } + } + } - // If we're inside a collection literal, let's recalculate the adjustment - // because the items inside the collection should not be subject to the same - // indentation as the brackets. - val adjustment = if (isInCollectionOrFunctionLiteral) { - val expectedPosition = intendedIndent.length + editorConfigIndent.indent.length - expectedPosition - child.column - } else { - argumentInnerIndentAdjustment - } + private fun wrapArgumentInList( + newIndentLevel: Int, + child: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean + ) { + val indent = "\n" + editorConfigIndent.indent.repeat(newIndentLevel) + when (child.elementType) { + ElementType.LPAR -> { + val prevLeaf = child.prevLeaf() + if (prevLeaf is PsiWhiteSpace && prevLeaf.textContains('\n')) { + emit(child.startOffset, errorMessage(child), true) + if (autoCorrect) { + prevLeaf.delete() + } + } + } + ElementType.VALUE_ARGUMENT, + ElementType.RPAR -> { + // aiming for + // ... LPAR + // VALUE_PARAMETER... + // RPAR + val intendedIndent = if (child.elementType == ElementType.VALUE_ARGUMENT) { + indent + editorConfigIndent.indent + } else { + indent + } - val split = n.text.split("\n") - (n as LeafElement).rawReplaceWithText( - split.joinToString("\n") { - when { - it.isEmpty() -> it - adjustment > 0 -> it + " ".repeat(adjustment) - else -> it.substring(0, max(it.length + adjustment, 0)) - } - } - ) - } - } - } + val prevLeaf = child.prevWhiteSpaceWithNewLine() ?: child.prevLeaf() + if (prevLeaf is PsiWhiteSpace) { + if (prevLeaf.getText().contains("\n")) { + // The current child is already wrapped to a new line. Checking and fixing the + // correct size of the indent is the responsibility of the IndentationRule. + return + } else { + // The current child needs to be wrapped to a newline. + emit(child.startOffset, errorMessage(child), true) + if (autoCorrect) { + // The indentation is purely based on the previous leaf only. Note that in + // autoCorrect mode the indent rule, if enabled, runs after this rule and + // determines the final indentation. But if the indent rule is disabled then the + // indent of this rule is kept. + (prevLeaf as LeafPsiElement).rawReplaceWithText(intendedIndent) } } + } else { + // Insert a new whitespace element in order to wrap the current child to a new line. + emit(child.startOffset, errorMessage(child), true) + if (autoCorrect) { + child.treeParent.addChild(PsiWhiteSpaceImpl(intendedIndent), child) + } } + // Indentation of child nodes need to be fixed by the IndentationRule. } } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ChainWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ChainWrappingRule.kt index 0e2eefe96d..10b50a900d 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ChainWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ChainWrappingRule.kt @@ -40,7 +40,7 @@ class ChainWrappingRule : Rule("chain-wrapping") { private val nextLineTokens = TokenSet.create(DOT, SAFE_ACCESS, ELVIS) private val noSpaceAroundTokens = TokenSet.create(DOT, SAFE_ACCESS) - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/CommentSpacingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/CommentSpacingRule.kt index cc5f260526..16554ae28d 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/CommentSpacingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/CommentSpacingRule.kt @@ -10,7 +10,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement class CommentSpacingRule : Rule("comment-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/EnumEntryNameCaseRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/EnumEntryNameCaseRule.kt index c779fc9f15..1071b42f94 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/EnumEntryNameCaseRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/EnumEntryNameCaseRule.kt @@ -13,7 +13,7 @@ public class EnumEntryNameCaseRule : Rule("enum-entry-name-case") { val regex = Regex("[A-Z]([A-Za-z\\d]*|[A-Z_\\d]*)") } - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRule.kt index 3c9565b1cd..947bb8ce91 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRule.kt @@ -11,6 +11,7 @@ import com.pinterest.ktlint.core.ast.ElementType.PROPERTY import com.pinterest.ktlint.core.ast.ElementType.TYPEALIAS import com.pinterest.ktlint.core.ast.ElementType.TYPE_REFERENCE import com.pinterest.ktlint.core.ast.children +import com.pinterest.ktlint.core.ast.isRoot import java.nio.file.Paths import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.lang.FileASTNode @@ -39,59 +40,56 @@ import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType * - file without `.kt` extension * - file with name `package.kt` */ -public class FilenameRule : Rule( - id = "filename", - visitorModifiers = setOf( - VisitorModifier.RunOnRootNodeOnly - ) -) { - override fun visit( +public class FilenameRule : Rule("filename") { + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { - node as FileASTNode? ?: error("node is not ${FileASTNode::class} but ${node::class}") + if (node.isRoot()) { + node as FileASTNode? ?: error("node is not ${FileASTNode::class} but ${node::class}") - val filePath = node.getUserData(KtLint.FILE_PATH_USER_DATA_KEY) - if (filePath?.endsWith(".kt") != true) { - // ignore all non ".kt" files (including ".kts") - return - } + val filePath = node.getUserData(KtLint.FILE_PATH_USER_DATA_KEY) + if (filePath?.endsWith(".kt") != true) { + // ignore all non ".kt" files (including ".kts") + return + } - val fileName = Paths.get(filePath).fileName.toString().substringBefore(".") - if (fileName == "package") { - // ignore package.kt filename - return - } + val fileName = Paths.get(filePath).fileName.toString().substringBefore(".") + if (fileName == "package") { + // ignore package.kt filename + return + } - val topLevelClassDeclarations = node.topLevelDeclarations(CLASS) - if (topLevelClassDeclarations.size == 1) { - val topLevelClassDeclaration = topLevelClassDeclarations.first() - if (node.hasTopLevelDeclarationNotExtending(topLevelClassDeclaration.identifier)) { - fileName.shouldMatchPascalCase(emit) + val topLevelClassDeclarations = node.topLevelDeclarations(CLASS) + if (topLevelClassDeclarations.size == 1) { + val topLevelClassDeclaration = topLevelClassDeclarations.first() + if (node.hasTopLevelDeclarationNotExtending(topLevelClassDeclaration.identifier)) { + fileName.shouldMatchPascalCase(emit) + } else { + // If the file only contains one (non private) top level class and possibly some extension functions of + // that class, then its filename should be identical to the class name. + fileName.shouldMatchClassName(topLevelClassDeclaration.identifier, emit) + } } else { - // If the file only contains one (non private) top level class and possibly some extension functions of - // that class, then its filename should be identical to the class name. - fileName.shouldMatchClassName(topLevelClassDeclaration.identifier, emit) - } - } else { - val topLevelDeclarations = node.topLevelDeclarations() - if (topLevelDeclarations.size == 1) { - val topLevelDeclaration = topLevelDeclarations.first() - if (topLevelDeclaration.elementType == OBJECT_DECLARATION || - topLevelDeclaration.elementType == TYPEALIAS || - topLevelDeclaration.elementType == FUN - ) { - val pascalCaseIdentifier = - topLevelDeclaration - .identifier - .toPascalCase() - fileName.shouldMatchFileName(pascalCaseIdentifier, emit) + val topLevelDeclarations = node.topLevelDeclarations() + if (topLevelDeclarations.size == 1) { + val topLevelDeclaration = topLevelDeclarations.first() + if (topLevelDeclaration.elementType == OBJECT_DECLARATION || + topLevelDeclaration.elementType == TYPEALIAS || + topLevelDeclaration.elementType == FUN + ) { + val pascalCaseIdentifier = + topLevelDeclaration + .identifier + .toPascalCase() + fileName.shouldMatchFileName(pascalCaseIdentifier, emit) + } else { + fileName.shouldMatchPascalCase(emit) + } } else { fileName.shouldMatchPascalCase(emit) } - } else { - fileName.shouldMatchPascalCase(emit) } } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/FinalNewlineRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/FinalNewlineRule.kt index a845fe2df0..e7a6044679 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/FinalNewlineRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/FinalNewlineRule.kt @@ -2,33 +2,35 @@ package com.pinterest.ktlint.ruleset.standard import com.pinterest.ktlint.core.Rule import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.insertNewLineProperty +import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import com.pinterest.ktlint.core.ast.isRoot +import kotlin.properties.Delegates 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.PsiWhiteSpaceImpl public class FinalNewlineRule : - Rule( - id = "final-newline", - visitorModifiers = setOf( - VisitorModifier.RunOnRootNodeOnly - ) - ), + Rule("final-newline"), UsesEditorConfigProperties { override val editorConfigProperties: List> = listOf( insertNewLineProperty ) - override fun visit( + private var insertFinalNewline by Delegates.notNull() + + override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) { + insertFinalNewline = editorConfigProperties.getEditorConfigValue(insertNewLineProperty) + } + + override fun afterVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { if (node.isRoot()) { if (node.textLength == 0) return - val insertFinalNewline = node.getEditorConfigValue(insertNewLineProperty) val lastNode = lastChildNodeOf(node) if (insertFinalNewline) { if (lastNode !is PsiWhiteSpace || !lastNode.textContains('\n')) { diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ImportOrderingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ImportOrderingRule.kt index fb0bb8fa3e..38755969b4 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ImportOrderingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ImportOrderingRule.kt @@ -1,9 +1,9 @@ package com.pinterest.ktlint.ruleset.standard import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import com.pinterest.ktlint.core.ast.ElementType -import com.pinterest.ktlint.core.ast.isRoot import com.pinterest.ktlint.core.initKtLintKLogger import com.pinterest.ktlint.ruleset.standard.ImportOrderingRule.Companion.ASCII_PATTERN import com.pinterest.ktlint.ruleset.standard.ImportOrderingRule.Companion.IDEA_PATTERN @@ -47,128 +47,16 @@ public class ImportOrderingRule : private lateinit var importsLayout: List private lateinit var importSorter: ImportSorter - public companion object { - internal const val IDEA_IMPORTS_LAYOUT_PROPERTY_NAME = "ij_kotlin_imports_layout" - private const val PROPERTY_DESCRIPTION = "Defines imports order layout for Kotlin files" - - /** - * Alphabetical with capital letters before lower case letters (e.g. Z before a). - * No blank lines between major groups (android, com, junit, net, org, java, javax). - * Single group regardless of import type. - * - * https://developer.android.com/kotlin/style-guide#import_statements - */ - private val ASCII_PATTERN = parseImportsLayout("*") - - /** - * Default IntelliJ IDEA style: Alphabetical with capital letters before lower case letters (e.g. Z before a), - * except such groups as "java", "javax" and "kotlin" that are placed in the end. Within the groups the alphabetical order is preserved. - * Alias imports are placed in a separate group in the end of the list with alphabetical order inside. - * No blank lines between groups. - * - * https://github.com/JetBrains/kotlin/blob/ffdab473e28d0d872136b910eb2e0f4beea2e19c/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinCodeStyleSettings.java#L87-L91 - */ - private val IDEA_PATTERN = parseImportsLayout("*,java.**,javax.**,kotlin.**,^") - - private const val IDEA_ERROR_MESSAGE = "Imports must be ordered in lexicographic order without any empty lines in-between " + - "with \"java\", \"javax\", \"kotlin\" and aliases in the end" - private const val ASCII_ERROR_MESSAGE = "Imports must be ordered in lexicographic order without any empty lines in-between" - private const val CUSTOM_ERROR_MESSAGE = "Imports must be ordered according to the pattern specified in .editorconfig" - - private val errorMessages = mapOf( - IDEA_PATTERN to IDEA_ERROR_MESSAGE, - ASCII_PATTERN to ASCII_ERROR_MESSAGE - ) - - private val editorConfigPropertyParser: (String, String?) -> PropertyType.PropertyValue> = - { _, value -> - when { - value.isNullOrBlank() -> PropertyType.PropertyValue.invalid( - value, - "Import layout must contain at least one entry of a wildcard symbol (*)" - ) - value == "idea" -> { - logger.warn { "`idea` is deprecated! Please use `*,java.**,javax.**,kotlin.**,^` instead to ensure that the Kotlin IDE plugin recognizes the value" } - PropertyType.PropertyValue.valid( - value, - IDEA_PATTERN - ) - } - value == "ascii" -> { - logger.warn { "`ascii` is deprecated! Please use `*` instead to ensure that the Kotlin IDE plugin recognizes the value" } - PropertyType.PropertyValue.valid( - value, - ASCII_PATTERN - ) - } - else -> try { - PropertyType.PropertyValue.valid( - value, - parseImportsLayout(value) - ) - } catch (e: IllegalArgumentException) { - PropertyType.PropertyValue.invalid( - value, - "Unexpected imports layout: $value" - ) - } - } - } - - public val ideaImportsLayoutProperty: UsesEditorConfigProperties.EditorConfigProperty> = - UsesEditorConfigProperties.EditorConfigProperty>( - type = PropertyType( - IDEA_IMPORTS_LAYOUT_PROPERTY_NAME, - PROPERTY_DESCRIPTION, - editorConfigPropertyParser - ), - defaultValue = IDEA_PATTERN, - defaultAndroidValue = ASCII_PATTERN, - propertyWriter = { it.joinToString(separator = ",") } - ) + override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) { + importsLayout = editorConfigProperties.getEditorConfigValue(ideaImportsLayoutProperty) + importSorter = ImportSorter(importsLayout) } - private fun getUniqueImportsAndBlankLines( - children: Array, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit - ): Pair> { - var autoCorrectDuplicateImports = false - val imports = mutableListOf() - val importTextSet = mutableSetOf() - - children.forEach { current -> - val isPsiWhiteSpace = current.psi is PsiWhiteSpace - - if (current.elementType == ElementType.IMPORT_DIRECTIVE || - isPsiWhiteSpace && current.textLength > 1 // also collect empty lines, that are represented as "\n\n" - ) { - if (isPsiWhiteSpace || importTextSet.add(current.text)) { - imports += current - } else { - emit( - current.startOffset, - "Duplicate '${current.text}' found", - true - ) - autoCorrectDuplicateImports = true - } - } - } - - return autoCorrectDuplicateImports to imports - } - - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { - if (node.isRoot()) { - importsLayout = node.getEditorConfigValue(ideaImportsLayoutProperty) - importSorter = ImportSorter(importsLayout) - return - } - if (node.elementType == ElementType.IMPORT_LIST) { val children = node.getChildren(null) if (children.isNotEmpty()) { @@ -239,6 +127,36 @@ public class ImportOrderingRule : } } + private fun getUniqueImportsAndBlankLines( + children: Array, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ): Pair> { + var autoCorrectDuplicateImports = false + val imports = mutableListOf() + val importTextSet = mutableSetOf() + + children.forEach { current -> + val isPsiWhiteSpace = current.psi is PsiWhiteSpace + + if (current.elementType == ElementType.IMPORT_DIRECTIVE || + isPsiWhiteSpace && current.textLength > 1 // also collect empty lines, that are represented as "\n\n" + ) { + if (isPsiWhiteSpace || importTextSet.add(current.text)) { + imports += current + } else { + emit( + current.startOffset, + "Duplicate '${current.text}' found", + true + ) + autoCorrectDuplicateImports = true + } + } + } + + return autoCorrectDuplicateImports to imports + } + private fun importsAreEqual(actual: List, expected: List): Boolean { if (actual.size != expected.size) return false @@ -256,4 +174,85 @@ public class ImportOrderingRule : private fun hasTooMuchWhitespace(nodes: Array): Boolean { return nodes.any { it is PsiWhiteSpace && (it as PsiWhiteSpace).text != "\n" } } + + public companion object { + internal const val IDEA_IMPORTS_LAYOUT_PROPERTY_NAME = "ij_kotlin_imports_layout" + private const val PROPERTY_DESCRIPTION = "Defines imports order layout for Kotlin files" + + /** + * Alphabetical with capital letters before lower case letters (e.g. Z before a). + * No blank lines between major groups (android, com, junit, net, org, java, javax). + * Single group regardless of import type. + * + * https://developer.android.com/kotlin/style-guide#import_statements + */ + private val ASCII_PATTERN = parseImportsLayout("*") + + /** + * Default IntelliJ IDEA style: Alphabetical with capital letters before lower case letters (e.g. Z before a), + * except such groups as "java", "javax" and "kotlin" that are placed in the end. Within the groups the alphabetical order is preserved. + * Alias imports are placed in a separate group in the end of the list with alphabetical order inside. + * No blank lines between groups. + * + * https://github.com/JetBrains/kotlin/blob/ffdab473e28d0d872136b910eb2e0f4beea2e19c/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinCodeStyleSettings.java#L87-L91 + */ + private val IDEA_PATTERN = parseImportsLayout("*,java.**,javax.**,kotlin.**,^") + + private const val IDEA_ERROR_MESSAGE = "Imports must be ordered in lexicographic order without any empty lines in-between " + + "with \"java\", \"javax\", \"kotlin\" and aliases in the end" + private const val ASCII_ERROR_MESSAGE = "Imports must be ordered in lexicographic order without any empty lines in-between" + private const val CUSTOM_ERROR_MESSAGE = "Imports must be ordered according to the pattern specified in .editorconfig" + + private val errorMessages = mapOf( + IDEA_PATTERN to IDEA_ERROR_MESSAGE, + ASCII_PATTERN to ASCII_ERROR_MESSAGE + ) + + private val editorConfigPropertyParser: (String, String?) -> PropertyType.PropertyValue> = + { _, value -> + when { + value.isNullOrBlank() -> PropertyType.PropertyValue.invalid( + value, + "Import layout must contain at least one entry of a wildcard symbol (*)" + ) + value == "idea" -> { + logger.warn { "`idea` is deprecated! Please use `*,java.**,javax.**,kotlin.**,^` instead to ensure that the Kotlin IDE plugin recognizes the value" } + PropertyType.PropertyValue.valid( + value, + IDEA_PATTERN + ) + } + value == "ascii" -> { + logger.warn { "`ascii` is deprecated! Please use `*` instead to ensure that the Kotlin IDE plugin recognizes the value" } + PropertyType.PropertyValue.valid( + value, + ASCII_PATTERN + ) + } + else -> try { + PropertyType.PropertyValue.valid( + value, + parseImportsLayout(value) + ) + } catch (e: IllegalArgumentException) { + PropertyType.PropertyValue.invalid( + value, + "Unexpected imports layout: $value" + ) + } + } + } + + public val ideaImportsLayoutProperty: UsesEditorConfigProperties.EditorConfigProperty> = + UsesEditorConfigProperties.EditorConfigProperty>( + type = PropertyType( + IDEA_IMPORTS_LAYOUT_PROPERTY_NAME, + PROPERTY_DESCRIPTION, + editorConfigPropertyParser + ), + defaultValue = IDEA_PATTERN, + defaultAndroidValue = ASCII_PATTERN, + propertyWriter = { it.joinToString(separator = ",") } + ) + } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt index 38e0afaa08..ecca5e07d9 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt @@ -6,6 +6,7 @@ import com.pinterest.ktlint.core.IndentConfig.IndentStyle.TAB import com.pinterest.ktlint.core.Rule import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.indentSizeProperty import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.indentStyleProperty +import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import com.pinterest.ktlint.core.ast.ElementType.ARROW import com.pinterest.ktlint.core.ast.ElementType.BINARY_EXPRESSION @@ -66,6 +67,7 @@ import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE import com.pinterest.ktlint.core.ast.children import com.pinterest.ktlint.core.ast.isPartOf import com.pinterest.ktlint.core.ast.isPartOfComment +import com.pinterest.ktlint.core.ast.isRoot import com.pinterest.ktlint.core.ast.isWhiteSpace import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline import com.pinterest.ktlint.core.ast.isWhiteSpaceWithoutNewline @@ -77,7 +79,6 @@ import com.pinterest.ktlint.core.ast.parent import com.pinterest.ktlint.core.ast.prevCodeLeaf import com.pinterest.ktlint.core.ast.prevCodeSibling import com.pinterest.ktlint.core.ast.prevLeaf -import com.pinterest.ktlint.core.ast.visit import com.pinterest.ktlint.core.initKtLintKLogger import com.pinterest.ktlint.ruleset.standard.IndentationRule.IndentContext.Block import com.pinterest.ktlint.ruleset.standard.IndentationRule.IndentContext.Block.BlockIndentationType.REGULAR @@ -95,17 +96,10 @@ import org.jetbrains.kotlin.psi.psiUtil.leaves private val logger = KotlinLogging.logger {}.initKtLintKLogger() -/** - * Checks & correct indentation - * - * Current limitations: - * - "all or nothing" (currently, rule can only be disabled for an entire file) - */ public class IndentationRule : Rule( id = "indent", visitorModifiers = setOf( - VisitorModifier.RunOnRootNodeOnly, VisitorModifier.RunAsLateAsPossible, VisitorModifier.RunAfterRule( ruleId = "experimental:function-signature", @@ -120,325 +114,278 @@ public class IndentationRule : indentSizeProperty, indentStyleProperty ) - - private companion object { - private val lTokenSet = TokenSet.create(LPAR, LBRACE, LBRACKET, LT) - private val rTokenSet = TokenSet.create(RPAR, RBRACE, RBRACKET, GT) - private val matchingRToken = - lTokenSet.types.zip( - rTokenSet.types - ).toMap() - } + private var indentConfig = IndentConfig.DEFAULT_INDENT_CONFIG private var line = 1 private var expectedIndent = 0 // TODO: merge into IndentContext - private fun reset() { - line = 1 - expectedIndent = 0 - } + private val ctx = IndentContext() - private var indentConfig = IndentConfig.DEFAULT_INDENT_CONFIG + override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) { + indentConfig = IndentConfig( + indentStyle = editorConfigProperties.getEditorConfigValue(indentStyleProperty), + tabWidth = editorConfigProperties.getEditorConfigValue(indentSizeProperty) + ) + } - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { - indentConfig = IndentConfig( - indentStyle = node.getEditorConfigValue(indentStyleProperty), - tabWidth = node.getEditorConfigValue(indentSizeProperty) - ) if (indentConfig.disabled) { return } - reset() - indent(node, autoCorrect, emit) - // The expectedIndent should never be negative. If so, it is very likely that ktlint crashes at runtime when - // autocorrecting is executed while no error occurs with linting only. Such errors often are not found in unit - // tests, as the examples are way more simple than realistic code. - assert(expectedIndent >= 0) - } - - private class IndentContext { - private val exitAdj = mutableMapOf() - val ignored = mutableSetOf() - val blockStack: Deque = LinkedList() - var localAdj: Int = 0 - - fun exitAdjBy(node: ASTNode, change: Int) { - exitAdj.compute(node) { _, v -> (v ?: 0) + change } - } - - fun clearExitAdj(node: ASTNode): Int? = - exitAdj.remove(node) - - data class Block( - // Element type used for opening the block - val openingElementType: IElementType, - // Line at which the block is opened - val line: Int, - // Type of indentation to be used for the block - val blockIndentationType: BlockIndentationType - ) { - enum class BlockIndentationType { - /** - * Indent the body of the block one level deeper by increasing the expected indentation level with 1. - * Decrease the expected indentation level just before the closing element of the block. - */ - REGULAR, - - /** - * Keep the indent of the body of the block identical to the indent of the previous block, so do not change - * the expected indentation level. The indentation of the closing element has to be decreased one level - * without altering the expected indentation level. - */ - SAME_AS_PREVIOUS_BLOCK + if (node.isRoot()) { + val firstNotEmptyLeaf = node.nextLeaf() + if (firstNotEmptyLeaf?.let { it.elementType == WHITE_SPACE && !it.textContains('\n') } == true) { + visitWhiteSpace(firstNotEmptyLeaf, autoCorrect, emit, ctx) } } - } - private fun indent( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit - ) { - val ctx = IndentContext() - val firstNotEmptyLeaf = node.nextLeaf() - if (firstNotEmptyLeaf?.let { it.elementType == WHITE_SPACE && !it.textContains('\n') } == true) { - visitWhiteSpace(firstNotEmptyLeaf, autoCorrect, emit, ctx) - } - node.visit( - { n -> - when (n.elementType) { - LPAR, LBRACE, LBRACKET -> { - // ({[ should increase expectedIndent by 1 - val prevBlock = ctx.blockStack.peek() - when { - n.isClosedOnSameLine() -> { - logger.trace { - "$line: block starting with ${n.text} is opened and closed on the same line, " + - "expected indent is kept unchanged -> $expectedIndent" - } - ctx.blockStack.push( - Block(n.elementType, line, SAME_AS_PREVIOUS_BLOCK) - ) - } - n.isAfterValueParameterOnSameLine() -> { - logger.trace { - "$line: block starting with ${n.text} starts on same line as the previous value " + - "parameter value ended, expected indent is kept unchanged -> $expectedIndent" - } - ctx.blockStack.push( - Block(n.elementType, line, SAME_AS_PREVIOUS_BLOCK) - ) - } - prevBlock != null && line == prevBlock.line -> { - logger.trace { - "$line: block starting with ${n.text} starts on same line as the previous block, " + - "expected indent is kept unchanged -> $expectedIndent" - } - ctx.blockStack.push( - Block(n.elementType, line, SAME_AS_PREVIOUS_BLOCK) - ) - } - else -> { - if (n.isPartOfForLoopConditionWithMultilineExpression()) { - logger.trace { "$line: block starting with ${n.text} -> Keep at $expectedIndent" } - ctx.blockStack.push( - Block(n.elementType, line, SAME_AS_PREVIOUS_BLOCK) - ) - } else { - expectedIndent++ - logger.trace { "$line: block starting with ${n.text} -> Increase to $expectedIndent" } - ctx.blockStack.push( - Block(n.elementType, line, REGULAR) - ) - } - } - } + when (node.elementType) { + LPAR, LBRACE, LBRACKET -> { + // ({[ should increase expectedIndent by 1 + val prevBlock = ctx.blockStack.peek() + when { + node.isClosedOnSameLine() -> { logger.trace { - ctx.blockStack.iterator().asSequence().toList() - .joinToString( - separator = "\n\t", - prefix = "Stack (newest first) after pushing new element:\n\t" - ) + "$line: block starting with ${node.text} is opened and closed on the same line, " + + "expected indent is kept unchanged -> $expectedIndent" } + ctx.blockStack.push( + Block(node.elementType, line, SAME_AS_PREVIOUS_BLOCK) + ) } - RPAR, RBRACE, RBRACKET -> { - // ]}) should decrease expectedIndent by 1 + node.isAfterValueParameterOnSameLine() -> { logger.trace { - ctx.blockStack.iterator().asSequence().toList() - .joinToString( - separator = "\n\t", - prefix = "Stack before popping newest element from top of stack:\n\t" - ) - } - val block = ctx.blockStack.pop() - when (block.blockIndentationType) { - SAME_AS_PREVIOUS_BLOCK -> { - logger.trace { "$line: block closed with ${n.elementType}. BlockIndentationType ${block.blockIndentationType} -> keep indent unchanged at $expectedIndent" } - } - REGULAR -> { - expectedIndent-- - logger.trace { "$line: block closed with ${n.elementType}. -> Decrease indent to $expectedIndent" } - } + "$line: block starting with ${node.text} starts on same line as the previous value " + + "parameter value ended, expected indent is kept unchanged -> $expectedIndent" } - - val pairedLeft = n.pairedLeft() - val byKeywordOnSameLine = pairedLeft.prevLeafOnSameLine(BY_KEYWORD) - if (byKeywordOnSameLine != null && - byKeywordOnSameLine.prevLeaf()?.isWhiteSpaceWithNewline() == true && - n.leavesOnSameLine(forward = true).all { it.isWhiteSpace() || it.isPartOfComment() } - ) { - expectedIndent-- - logger.trace { "$line: --on same line as by keyword ${n.text} -> $expectedIndent" } + ctx.blockStack.push( + Block(node.elementType, line, SAME_AS_PREVIOUS_BLOCK) + ) + } + prevBlock != null && line == prevBlock.line -> { + logger.trace { + "$line: block starting with ${node.text} starts on same line as the previous block, " + + "expected indent is kept unchanged -> $expectedIndent" } + ctx.blockStack.push( + Block(node.elementType, line, SAME_AS_PREVIOUS_BLOCK) + ) } - LT -> - // - if (n.treeParent.elementType.let { it == TYPE_PARAMETER_LIST || it == TYPE_ARGUMENT_LIST }) { + else -> { + if (node.isPartOfForLoopConditionWithMultilineExpression()) { + logger.trace { "$line: block starting with ${node.text} -> Keep at $expectedIndent" } + ctx.blockStack.push( + Block(node.elementType, line, SAME_AS_PREVIOUS_BLOCK) + ) + } else { expectedIndent++ - logger.trace { "$line: ++${n.text} -> $expectedIndent" } + logger.trace { "$line: block starting with ${node.text} -> Increase to $expectedIndent" } + ctx.blockStack.push( + Block(node.elementType, line, REGULAR) + ) } - GT -> - // - if (n.treeParent.elementType.let { it == TYPE_PARAMETER_LIST || it == TYPE_ARGUMENT_LIST }) { - expectedIndent-- - logger.trace { "$line: --${n.text} -> $expectedIndent" } - } - SUPER_TYPE_LIST -> - // class A : - // SUPER_TYPE_LIST - adjustExpectedIndentInsideSuperTypeList(n) - SUPER_TYPE_CALL_ENTRY, DELEGATED_SUPER_TYPE_ENTRY -> { - // IDEA quirk: - // - // class A : B({ - // f() {} - // }), - // C({ - // f() {} - // }) - // - // instead of expected - // - // class A : B({ - // f() {} - // }), - // C({ - // f() {} - // }) - adjustExpectedIndentInsideSuperTypeCall(n, ctx) } - STRING_TEMPLATE -> - indentStringTemplate(n, autoCorrect, emit) - DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION, BINARY_EXPRESSION, BINARY_WITH_TYPE -> { - val prevBlock = ctx.blockStack.peek() - if (prevBlock != null && prevBlock.line == line) { - ctx.ignored.add(n) - } + } + logger.trace { + ctx.blockStack.iterator().asSequence().toList() + .joinToString( + separator = "\n\t", + prefix = "Stack (newest first) after pushing new element:\n\t" + ) + } + } + RPAR, RBRACE, RBRACKET -> { + // ]}) should decrease expectedIndent by 1 + logger.trace { + ctx.blockStack.iterator().asSequence().toList() + .joinToString( + separator = "\n\t", + prefix = "Stack before popping newest element from top of stack:\n\t" + ) + } + val block = ctx.blockStack.pop() + when (block.blockIndentationType) { + SAME_AS_PREVIOUS_BLOCK -> { + logger.trace { "$line: block closed with ${node.elementType}. BlockIndentationType ${block.blockIndentationType} -> keep indent unchanged at $expectedIndent" } } - FUNCTION_LITERAL -> - adjustExpectedIndentInFunctionLiteral(n, ctx) - WHITE_SPACE -> - if (n.textContains('\n')) { - if ( - !n.isPartOfComment() && - !n.isPartOfTypeConstraint() // FIXME IndentationRuleTest.testLintWhereClause not checked - ) { - val p = n.treeParent - val nextSibling = n.treeNext - val prevLeaf = n.prevLeaf { !it.isPartOfComment() && !it.isWhiteSpaceWithoutNewline() } - when { - p.elementType.let { - it == DOT_QUALIFIED_EXPRESSION || it == SAFE_ACCESS_EXPRESSION - } -> - // value - // .x() - // .y - adjustExpectedIndentInsideQualifiedExpression(n, ctx) - p.elementType.let { - it == BINARY_EXPRESSION || it == BINARY_WITH_TYPE - } -> - // value - // + x() - // + y - adjustExpectedIndentInsideBinaryExpression(n, ctx) - nextSibling?.elementType.let { - it == THEN || it == ELSE || it == BODY - } -> - // if (...) - // THEN - // else - // ELSE - // while (...) - // BODY - adjustExpectedIndentInFrontOfControlBlock(n, ctx) - nextSibling?.elementType == PROPERTY_ACCESSOR -> - // val f: Type = - // PROPERTY_ACCESSOR get() = ... - // PROPERTY_ACCESSOR set() = ... - adjustExpectedIndentInFrontOfPropertyAccessor(n, ctx) - nextSibling?.elementType == SUPER_TYPE_LIST -> - // class C : - // SUPER_TYPE_LIST - adjustExpectedIndentInFrontOfSuperTypeList(n, ctx) - prevLeaf?.elementType == EQ && p.elementType != VALUE_ARGUMENT -> - // v = - // value - adjustExpectedIndentAfterEq(n, ctx) - prevLeaf?.elementType == ARROW -> - // when { - // v -> - // value - // } - adjustExpectedIndentAfterArrow(n, ctx) - prevLeaf?.elementType == COLON -> - // fun fn(): - // Int - adjustExpectedIndentAfterColon(n, ctx) - prevLeaf?.elementType == LPAR && - p.elementType == VALUE_ARGUMENT_LIST && - p.parent(CONDITION)?.takeIf { !it.prevLeaf().isWhiteSpaceWithNewline() } != null -> - // if (condition( - // params - // ) - // ) - adjustExpectedIndentAfterLparInsideCondition(n, ctx) - } - visitWhiteSpace(n, autoCorrect, emit, ctx) - if (ctx.localAdj != 0) { - expectedIndent += ctx.localAdj - logger.trace { "$line: ++${ctx.localAdj} on whitespace containing new line (${n.elementType}) -> $expectedIndent" } - ctx.localAdj = 0 - } - } else if (n.isPartOf(KDOC)) { - visitWhiteSpace(n, autoCorrect, emit, ctx) - } - line += n.text.count { it == '\n' } - } - EOL_COMMENT -> - if (n.text == "// ktlint-debug-print-expected-indent") { - logger.trace { "$line: expected indent: $expectedIndent" } - } + REGULAR -> { + expectedIndent-- + logger.trace { "$line: block closed with ${node.elementType}. -> Decrease indent to $expectedIndent" } + } + } + + val pairedLeft = node.pairedLeft() + val byKeywordOnSameLine = pairedLeft.prevLeafOnSameLine(BY_KEYWORD) + if (byKeywordOnSameLine != null && + byKeywordOnSameLine.prevLeaf()?.isWhiteSpaceWithNewline() == true && + node.leavesOnSameLine(forward = true).all { it.isWhiteSpace() || it.isPartOfComment() } + ) { + expectedIndent-- + logger.trace { "$line: --on same line as by keyword ${node.text} -> $expectedIndent" } + } + } + LT -> + // + if (node.treeParent.elementType.let { it == TYPE_PARAMETER_LIST || it == TYPE_ARGUMENT_LIST }) { + expectedIndent++ + logger.trace { "$line: ++${node.text} -> $expectedIndent" } } - }, - { n -> - when (n.elementType) { - SUPER_TYPE_LIST -> - adjustExpectedIndentAfterSuperTypeList(n) - DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION, BINARY_EXPRESSION, BINARY_WITH_TYPE -> - ctx.ignored.remove(n) + GT -> + // + if (node.treeParent.elementType.let { it == TYPE_PARAMETER_LIST || it == TYPE_ARGUMENT_LIST }) { + expectedIndent-- + logger.trace { "$line: --${node.text} -> $expectedIndent" } } - val adj = ctx.clearExitAdj(n) - if (adj != null) { - expectedIndent += adj - logger.trace { "$line: adjusted ${n.elementType} by $adj -> $expectedIndent" } + SUPER_TYPE_LIST -> + // class A : + // SUPER_TYPE_LIST + adjustExpectedIndentInsideSuperTypeList(node) + SUPER_TYPE_CALL_ENTRY, DELEGATED_SUPER_TYPE_ENTRY -> { + // IDEA quirk: + // + // class A : B({ + // f() {} + // }), + // C({ + // f() {} + // }) + // + // instead of expected + // + // class A : B({ + // f() {} + // }), + // C({ + // f() {} + // }) + adjustExpectedIndentInsideSuperTypeCall(node, ctx) + } + STRING_TEMPLATE -> + indentStringTemplate(node, autoCorrect, emit) + DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION, BINARY_EXPRESSION, BINARY_WITH_TYPE -> { + val prevBlock = ctx.blockStack.peek() + if (prevBlock != null && prevBlock.line == line) { + ctx.ignored.add(node) } } - ) + FUNCTION_LITERAL -> + adjustExpectedIndentInFunctionLiteral(node, ctx) + WHITE_SPACE -> + if (node.textContains('\n')) { + if ( + !node.isPartOfComment() && + !node.isPartOfTypeConstraint() // FIXME IndentationRuleTest.testLintWhereClause not checked + ) { + val p = node.treeParent + val nextSibling = node.treeNext + val prevLeaf = node.prevLeaf { !it.isPartOfComment() && !it.isWhiteSpaceWithoutNewline() } + when { + p.elementType.let { + it == DOT_QUALIFIED_EXPRESSION || it == SAFE_ACCESS_EXPRESSION + } -> + // value + // .x() + // .y + adjustExpectedIndentInsideQualifiedExpression(node, ctx) + p.elementType.let { + it == BINARY_EXPRESSION || it == BINARY_WITH_TYPE + } -> + // value + // + x() + // + y + adjustExpectedIndentInsideBinaryExpression(node, ctx) + nextSibling?.elementType.let { + it == THEN || it == ELSE || it == BODY + } -> + // if (...) + // THEN + // else + // ELSE + // while (...) + // BODY + adjustExpectedIndentInFrontOfControlBlock(node, ctx) + nextSibling?.elementType == PROPERTY_ACCESSOR -> + // val f: Type = + // PROPERTY_ACCESSOR get() = ... + // PROPERTY_ACCESSOR set() = ... + adjustExpectedIndentInFrontOfPropertyAccessor(node, ctx) + nextSibling?.elementType == SUPER_TYPE_LIST -> + // class C : + // SUPER_TYPE_LIST + adjustExpectedIndentInFrontOfSuperTypeList(node, ctx) + prevLeaf?.elementType == EQ && p.elementType != VALUE_ARGUMENT -> + // v = + // value + adjustExpectedIndentAfterEq(node, ctx) + prevLeaf?.elementType == ARROW -> + // when { + // v -> + // value + // } + adjustExpectedIndentAfterArrow(node, ctx) + prevLeaf?.elementType == COLON -> + // fun fn(): + // Int + adjustExpectedIndentAfterColon(node, ctx) + prevLeaf?.elementType == LPAR && + p.elementType == VALUE_ARGUMENT_LIST && + p.parent(CONDITION)?.takeIf { !it.prevLeaf().isWhiteSpaceWithNewline() } != null -> + // if (condition( + // params + // ) + // ) + adjustExpectedIndentAfterLparInsideCondition(node, ctx) + } + visitWhiteSpace(node, autoCorrect, emit, ctx) + if (ctx.localAdj != 0) { + expectedIndent += ctx.localAdj + logger.trace { "$line: ++${ctx.localAdj} on whitespace containing new line (${node.elementType}) -> $expectedIndent" } + ctx.localAdj = 0 + } + } else if (node.isPartOf(KDOC)) { + visitWhiteSpace(node, autoCorrect, emit, ctx) + } + line += node.text.count { it == '\n' } + } + EOL_COMMENT -> + if (node.text == "// ktlint-debug-print-expected-indent") { + logger.trace { "$line: expected indent: $expectedIndent" } + } + } + } + + override fun afterVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (indentConfig.disabled) { + return + } + + when (node.elementType) { + SUPER_TYPE_LIST -> + adjustExpectedIndentAfterSuperTypeList(node) + DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION, BINARY_EXPRESSION, BINARY_WITH_TYPE -> + ctx.ignored.remove(node) + } + val adj = ctx.clearExitAdj(node) + if (adj != null) { + expectedIndent += adj + logger.trace { "$line: adjusted ${node.elementType} by $adj -> $expectedIndent" } + } + } + + override fun afterLastNode() { + // The expectedIndent should never be negative. If so, it is very likely that ktlint crashes at runtime when + // autocorrecting is executed while no error occurs with linting only. Such errors often are not found in unit + // tests, as the examples are way more simple than realistic code. + assert(expectedIndent >= 0) } private fun adjustExpectedIndentInsideQualifiedExpression(n: ASTNode, ctx: IndentContext) { @@ -1009,6 +956,53 @@ public class IndentationRule : } return true } + + private class IndentContext { + private val exitAdj = mutableMapOf() + val ignored = mutableSetOf() + val blockStack: Deque = LinkedList() + var localAdj: Int = 0 + + fun exitAdjBy(node: ASTNode, change: Int) { + exitAdj.compute(node) { _, v -> (v ?: 0) + change } + } + + fun clearExitAdj(node: ASTNode): Int? = + exitAdj.remove(node) + + data class Block( + // Element type used for opening the block + val openingElementType: IElementType, + // Line at which the block is opened + val line: Int, + // Type of indentation to be used for the block + val blockIndentationType: BlockIndentationType + ) { + enum class BlockIndentationType { + /** + * Indent the body of the block one level deeper by increasing the expected indentation level with 1. + * Decrease the expected indentation level just before the closing element of the block. + */ + REGULAR, + + /** + * Keep the indent of the body of the block identical to the indent of the previous block, so do not change + * the expected indentation level. The indentation of the closing element has to be decreased one level + * without altering the expected indentation level. + */ + SAME_AS_PREVIOUS_BLOCK + } + } + } + + private companion object { + private val lTokenSet = TokenSet.create(LPAR, LBRACE, LBRACKET, LT) + private val rTokenSet = TokenSet.create(RPAR, RBRACE, RBRACKET, GT) + private val matchingRToken = + lTokenSet.types.zip( + rTokenSet.types + ).toMap() + } } private fun ASTNode.isKDocIndent() = diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/MaxLineLengthRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/MaxLineLengthRule.kt index 812d9abeea..0ecdb55be5 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/MaxLineLengthRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/MaxLineLengthRule.kt @@ -2,6 +2,7 @@ package com.pinterest.ktlint.ruleset.standard import com.pinterest.ktlint.core.Rule import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.maxLineLengthProperty +import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import com.pinterest.ktlint.core.ast.ElementType import com.pinterest.ktlint.core.ast.isPartOf @@ -9,6 +10,7 @@ import com.pinterest.ktlint.core.ast.isRoot import com.pinterest.ktlint.core.ast.nextLeaf import com.pinterest.ktlint.core.ast.parent import com.pinterest.ktlint.core.ast.prevCodeSibling +import kotlin.properties.Delegates import org.ec4j.core.model.PropertyType import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiComment @@ -42,18 +44,22 @@ class MaxLineLengthRule : private var maxLineLength: Int = maxLineLengthProperty.defaultValue private var rangeTree = RangeTree() + private var ignoreBackTickedIdentifier by Delegates.notNull() - override fun visit( + override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) { + ignoreBackTickedIdentifier = editorConfigProperties.getEditorConfigValue(ignoreBackTickedIdentifierProperty) + maxLineLength = editorConfigProperties.getEditorConfigValue(maxLineLengthProperty) + } + + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { + if (maxLineLength <= 0) { + return + } if (node.isRoot()) { - val ignoreBackTickedIdentifier = node.getEditorConfigValue(ignoreBackTickedIdentifierProperty) - maxLineLength = node.getEditorConfigValue(maxLineLengthProperty) - if (maxLineLength <= 0) { - return - } val errorOffset = arrayListOf() node .getElementsPerLine() @@ -160,6 +166,7 @@ private data class ParsedLine( } } +@Deprecated("Marked for removal from public API in ktlint 0.48") class RangeTree(seq: List = emptyList()) { private var emptyArrayView = ArrayView(0, 0) @@ -173,6 +180,7 @@ class RangeTree(seq: List = emptyList()) { // runtime: O(log(n)+k), where k is number of matching points // space: O(1) + @Deprecated("Marked for removal from public API in ktlint 0.48") fun query(vmin: Int, vmax: Int): RangeTree.ArrayView { var r = arr.size - 1 if (r == -1 || vmax < arr[0] || arr[r] < vmin) { @@ -206,12 +214,16 @@ class RangeTree(seq: List = emptyList()) { return ArrayView(l, k) } + @Deprecated("Marked for removal from public API in ktlint 0.48") fun isEmpty() = arr.isEmpty() + @Deprecated("Marked for removal from public API in ktlint 0.48") inner class ArrayView(private var l: Int, private val r: Int) { + @Deprecated("Marked for removal from public API in ktlint 0.48") val size: Int = r - l + @Deprecated("Marked for removal from public API in ktlint 0.48") fun get(i: Int): Int { if (i < 0 || i >= size) { throw IndexOutOfBoundsException() @@ -219,6 +231,7 @@ class RangeTree(seq: List = emptyList()) { return arr[l + i] } + @Deprecated("Marked for removal from public API in ktlint 0.48") inline fun forEach(cb: (v: Int) -> Unit) { var i = 0 while (i < size) { diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ModifierOrderRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ModifierOrderRule.kt index 6ce607e61d..294f000fd5 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ModifierOrderRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ModifierOrderRule.kt @@ -59,7 +59,7 @@ class ModifierOrderRule : Rule("modifier-order") { ) private val tokenSet = TokenSet.create(*order) - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/MultiLineIfElseRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/MultiLineIfElseRule.kt index 8159720797..ac5c93f170 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/MultiLineIfElseRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/MultiLineIfElseRule.kt @@ -21,7 +21,7 @@ import org.jetbrains.kotlin.psi.psiUtil.leaves * https://kotlinlang.org/docs/reference/coding-conventions.html#formatting-control-flow-statements */ class MultiLineIfElseRule : Rule("multiline-if-else") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoBlankLineBeforeRbraceRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoBlankLineBeforeRbraceRule.kt index d1e118b9c8..56500143ff 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoBlankLineBeforeRbraceRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoBlankLineBeforeRbraceRule.kt @@ -9,7 +9,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement class NoBlankLineBeforeRbraceRule : Rule("no-blank-line-before-rbrace") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoBlankLinesInChainedMethodCallsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoBlankLinesInChainedMethodCallsRule.kt index f6b08ac0ce..253a2fb2c9 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoBlankLinesInChainedMethodCallsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoBlankLinesInChainedMethodCallsRule.kt @@ -7,7 +7,7 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement public class NoBlankLinesInChainedMethodCallsRule : Rule("no-blank-lines-in-chained-method-calls") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoConsecutiveBlankLinesRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoConsecutiveBlankLinesRule.kt index 174a023dd9..a075327fc4 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoConsecutiveBlankLinesRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoConsecutiveBlankLinesRule.kt @@ -11,7 +11,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement public class NoConsecutiveBlankLinesRule : Rule("no-consecutive-blank-lines") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoEmptyClassBodyRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoEmptyClassBodyRule.kt index ef1c7b0f6a..fce05fe898 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoEmptyClassBodyRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoEmptyClassBodyRule.kt @@ -13,7 +13,7 @@ import org.jetbrains.kotlin.psi.KtObjectLiteralExpression class NoEmptyClassBodyRule : Rule("no-empty-class-body") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoEmptyFirstLineInMethodBlockRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoEmptyFirstLineInMethodBlockRule.kt index 7f304e27b8..7520800695 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoEmptyFirstLineInMethodBlockRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoEmptyFirstLineInMethodBlockRule.kt @@ -11,7 +11,7 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement class NoEmptyFirstLineInMethodBlockRule : Rule("no-empty-first-line-in-method-block") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoLineBreakAfterElseRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoLineBreakAfterElseRule.kt index f80676488c..af40bc5463 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoLineBreakAfterElseRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoLineBreakAfterElseRule.kt @@ -12,7 +12,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement class NoLineBreakAfterElseRule : Rule("no-line-break-after-else") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoLineBreakBeforeAssignmentRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoLineBreakBeforeAssignmentRule.kt index ee39ef54f5..abc263507e 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoLineBreakBeforeAssignmentRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoLineBreakBeforeAssignmentRule.kt @@ -14,7 +14,7 @@ import org.jetbrains.kotlin.psi.psiUtil.siblings class NoLineBreakBeforeAssignmentRule : Rule("no-line-break-before-assignment") { - override fun visit(node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { + override fun beforeVisitChildNodes(node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { if (node.elementType == EQ) { val prevCodeSibling = node.prevCodeSibling() val hasLineBreakBeforeAssignment = prevCodeSibling diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoMultipleSpacesRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoMultipleSpacesRule.kt index 2fdae8973b..1c49f27487 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoMultipleSpacesRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoMultipleSpacesRule.kt @@ -8,7 +8,7 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement public class NoMultipleSpacesRule : Rule("no-multi-spaces") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoSemicolonsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoSemicolonsRule.kt index e28f05badf..e6b3e28a42 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoSemicolonsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoSemicolonsRule.kt @@ -29,7 +29,7 @@ import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType class NoSemicolonsRule : Rule("no-semi") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoTrailingSpacesRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoTrailingSpacesRule.kt index 20985fb71d..1dd0ad31f2 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoTrailingSpacesRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoTrailingSpacesRule.kt @@ -11,7 +11,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.kdoc.psi.api.KDoc class NoTrailingSpacesRule : Rule("no-trailing-spaces") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoUnitReturnRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoUnitReturnRule.kt index b1eed4c356..12d3b665cd 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoUnitReturnRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoUnitReturnRule.kt @@ -10,7 +10,7 @@ import org.jetbrains.kotlin.com.intellij.lang.ASTNode class NoUnitReturnRule : Rule("no-unit-return") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoUnusedImportsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoUnusedImportsRule.kt index 4aa1f10cc7..597ded4d23 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoUnusedImportsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoUnusedImportsRule.kt @@ -1,8 +1,10 @@ package com.pinterest.ktlint.ruleset.standard import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.ast.ElementType.BY_KEYWORD import com.pinterest.ktlint.core.ast.ElementType.DOT_QUALIFIED_EXPRESSION +import com.pinterest.ktlint.core.ast.ElementType.FILE import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER import com.pinterest.ktlint.core.ast.ElementType.IMPORT_DIRECTIVE import com.pinterest.ktlint.core.ast.ElementType.OPERATION_REFERENCE @@ -10,189 +12,218 @@ import com.pinterest.ktlint.core.ast.ElementType.PACKAGE_DIRECTIVE import com.pinterest.ktlint.core.ast.ElementType.REFERENCE_EXPRESSION import com.pinterest.ktlint.core.ast.isPartOf import com.pinterest.ktlint.core.ast.isRoot -import com.pinterest.ktlint.core.ast.visit +import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline +import com.pinterest.ktlint.core.ast.nextLeaf +import com.pinterest.ktlint.core.ast.nextSibling +import com.pinterest.ktlint.core.ast.prevSibling import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement -import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType -import org.jetbrains.kotlin.kdoc.lexer.KDocTokens +import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.MARKDOWN_LINK import org.jetbrains.kotlin.kdoc.psi.impl.KDocLink import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtImportDirective import org.jetbrains.kotlin.psi.KtPackageDirective import org.jetbrains.kotlin.resolve.ImportPath +import org.jetbrains.kotlin.utils.addToStdlib.safeAs -class NoUnusedImportsRule : Rule("no-unused-imports") { - - private val componentNRegex = Regex("^component\\d+$") - - private val operatorSet = setOf( - // unary - "unaryPlus", "unaryMinus", "not", - // inc/dec - "inc", "dec", - // arithmetic - "plus", "minus", "times", "div", "rem", "mod", "rangeTo", - // in - "contains", - // indexed access - "get", "set", - // invoke - "invoke", - // augmented assignments - "plusAssign", "minusAssign", "timesAssign", "divAssign", "modAssign", - // (in)equality - "equals", - // comparison - "compareTo", - // iteration (https://github.com/shyiko/ktlint/issues/40) - "iterator", - // by (https://github.com/shyiko/ktlint/issues/54) - "getValue", "setValue" - ) - - private val conditionalWhitelist = mapOf<(String) -> Boolean, (node: ASTNode) -> Boolean>( - Pair( - // Ignore provideDelegate if there is a `by` anywhere in the file - { importPath -> importPath.endsWith(".provideDelegate") }, - { rootNode -> - var hasByKeyword = false - rootNode.visit { child -> - if (child.elementType == BY_KEYWORD) { - hasByKeyword = true - return@visit - } - } - hasByKeyword - } - ) - ) - - private data class Reference(val text: String, val inDotQualifiedExpression: Boolean) +public class NoUnusedImportsRule : Rule("no-unused-imports") { private val ref = mutableSetOf() private val parentExpressions = mutableSetOf() - private val imports = mutableSetOf() - private val usedImportPaths = mutableSetOf() + private val imports = mutableMapOf() private var packageName = "" private var rootNode: ASTNode? = null + private var foundByKeyword = false + + override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) { + // Rule can potentially be executed more than once (when formatting), so clear state first + ref.clear() + ref.add(Reference("*", false)) + parentExpressions.clear() + imports.clear() + foundByKeyword = false + } - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { if (node.isRoot()) { rootNode = node - ref.clear() // rule can potentially be executed more than once (when formatting) - ref.add(Reference("*", false)) - parentExpressions.clear() - imports.clear() - usedImportPaths.clear() - node.visit { vnode -> - val psi = vnode.psi - val type = vnode.elementType - val text = vnode.text - if (checkIfExpressionHasParentImport(text, type)) { - parentExpressions.add(text.substringBeforeLast("(")) + } + when (node.elementType) { + PACKAGE_DIRECTIVE -> { + val packageDirective = node.psi as KtPackageDirective + packageName = packageDirective.qualifiedName + } + IMPORT_DIRECTIVE -> { + val importPath = (node.psi as KtImportDirective).importPath!! + if (imports.containsKey(importPath)) { + // Emit directly when same import occurs more than once + emit(node.startOffset, "Unused import", true) + if (autoCorrect) { + node.psi.delete() + } + } else { + imports[importPath] = node } - if (type == KDocTokens.MARKDOWN_LINK && psi is KDocLink) { - val linkText = psi.getLinkText().removeBackticks() - ref.add(Reference(linkText.split('.').first(), false)) - ref.add(Reference(linkText.split('.').last(), false)) - } else if ((type == REFERENCE_EXPRESSION || type == OPERATION_REFERENCE) && - !vnode.isPartOf(IMPORT_DIRECTIVE) - ) { - val identifier = if (vnode is CompositeElement) { - vnode.findChildByType(IDENTIFIER) + } + DOT_QUALIFIED_EXPRESSION -> { + if (node.isExpressionForStaticImportWithExistingParentImport()) { + parentExpressions.add(node.text.substringBeforeLast("(")) + } + } + MARKDOWN_LINK -> { + node + .psi + .safeAs() + ?.let { kdocLink -> + val linkText = kdocLink.getLinkText().removeBackticksAndTrim() + ref.add(Reference(linkText.split('.').first(), false)) + ref.add(Reference(linkText.split('.').last(), false)) + } + } + REFERENCE_EXPRESSION, OPERATION_REFERENCE -> { + if (!node.isPartOf(IMPORT_DIRECTIVE)) { + val identifier = if (node is CompositeElement) { + node.findChildByType(IDENTIFIER) } else { - vnode + node } identifier ?.let { identifier.text } ?.takeIf { it.isNotBlank() } ?.let { - ref.add(Reference(it.removeBackticks(), psi.parentDotQualifiedExpression() != null)) - } - } else if (type == IMPORT_DIRECTIVE) { - val importPath = (vnode.psi as KtImportDirective).importPath!! - if (!usedImportPaths.add(importPath)) { - emit(vnode.startOffset, "Unused import", true) - if (autoCorrect) { - vnode.psi.delete() + ref.add( + Reference( + it.removeBackticksAndTrim(), + node.psi.parentDotQualifiedExpression() != null + ) + ) } - } else { - imports += importPath.pathStr.removeBackticks().trim() - } } } + BY_KEYWORD -> foundByKeyword = true + } + } + + override fun afterVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.elementType == FILE) { val directCalls = ref.filter { !it.inDotQualifiedExpression }.map { it.text } parentExpressions.forEach { parent -> - imports.removeIf { imp -> - imp.endsWith(".$parent") && directCalls.none { imp.endsWith(".$it") } - } + imports + .filterKeys { import -> + val importPath = import.pathStr.removeBackticksAndTrim() + importPath.endsWith(".$parent") && directCalls.none { importPath.endsWith(".$it") } + }.forEach { (importPath, importNode) -> + emit(importNode.startOffset, "Unused import", true) + if (autoCorrect) { + imports.remove(importPath, importNode) + importNode.removeImportDirective() + } + } } - } else if (node.elementType == PACKAGE_DIRECTIVE) { - val packageDirective = node.psi as KtPackageDirective - packageName = packageDirective.qualifiedName - } else if (node.elementType == IMPORT_DIRECTIVE) { - val importDirective = node.psi as KtImportDirective - val name = importDirective.importPath?.importedName?.asString()?.removeBackticks() - val importPath = importDirective.importPath?.pathStr?.removeBackticks()!! - if (importDirective.aliasName == null && - (packageName.isEmpty() || importPath.startsWith("$packageName.")) && - importPath.substring(packageName.length + 1).indexOf('.') == -1 - ) { - emit(node.startOffset, "Unnecessary import", true) - if (autoCorrect) { - importDirective.delete() - } - } else if (name != null && (!ref.map { it.text }.contains(name) || !isAValidImport(importPath)) && - !operatorSet.contains(name) && - !name.isComponentN() && - conditionalWhitelist - .filterKeys { selector -> selector(importPath) } - .none { (_, condition) -> condition(rootNode!!) } - ) { - emit(node.startOffset, "Unused import", true) - if (autoCorrect) { - importDirective.delete() + + imports.forEach { (_, node) -> + val importDirective = node.psi as KtImportDirective + val name = importDirective.importPath?.importedName?.asString()?.removeBackticksAndTrim() + val importPath = importDirective.importPath?.pathStr?.removeBackticksAndTrim()!! + if (importDirective.aliasName == null && + (packageName.isEmpty() || importPath.startsWith("$packageName.")) && + importPath.substring(packageName.length + 1).indexOf('.') == -1 + ) { + emit(node.startOffset, "Unnecessary import", true) + if (autoCorrect) { + importDirective.delete() + } + } else if (name != null && (!ref.map { it.text }.contains(name) || !isAValidImport(importPath)) && + !operatorSet.contains(name) && + !name.isComponentN() && + !importPath.ignoreProvideDelegate() + ) { + emit(node.startOffset, "Unused import", true) + if (autoCorrect) { + importDirective.delete() + } } } } } - // Checks if any static method call is present in code with the parent import present in imports list - private fun checkIfExpressionHasParentImport(text: String, type: IElementType): Boolean { - val containsMethodCall = text.split(".").last().contains("(") - return type == DOT_QUALIFIED_EXPRESSION && containsMethodCall && checkIfParentImportExists(text.substringBeforeLast("(")) + private fun String.ignoreProvideDelegate() = + if (endsWith(".provideDelegate")) { + // Ignore provideDelegate if the `by` keyword is found anywhere in the file + foundByKeyword + } else { + false + } + + private fun ASTNode.removeImportDirective() { + require(this.elementType == IMPORT_DIRECTIVE) + when { + treeParent.firstChildNode == this -> { + nextSibling { true } + ?.takeIf { it.isWhiteSpaceWithNewline() } + ?.let { it.treeParent.removeChild(it) } + } + treeParent.lastChildNode == this -> { + prevSibling { true } + ?.takeIf { it.isWhiteSpaceWithNewline() } + ?.let { it.treeParent.removeChild(it) } + } + else -> { + nextLeaf(true) + ?.takeIf { it.isWhiteSpaceWithNewline() } + ?.let { it.treeParent.removeChild(it) } + } + } + treeParent.removeChild(this) } - // Contains list of all imports and checks if given import is present - private fun checkIfParentImportExists(text: String): Boolean { + private fun ASTNode.isExpressionForStaticImportWithExistingParentImport(): Boolean { + if (!containsMethodCall()) { + return false + } + + val methodCallExpression = text.substringBeforeLast( + "(" + ) + // Only check static imports; identified if they start with a capital letter indicating a // class name rather than a sub-package - if (text.isNotEmpty() && text[0] !in 'A'..'Z') { + if (methodCallExpression.isNotEmpty() && methodCallExpression[0] !in 'A'..'Z') { return false } - val staticImports = imports.filter { it.endsWith(".$text") } - staticImports.forEach { import -> - val count = imports.count { - it.startsWith(import.substringBefore(text)) - } - // Parent import and static import both are present - if (count > 1) { - return true + imports + .filterKeys { it.pathStr.removeBackticksAndTrim().endsWith(".$methodCallExpression") } + .forEach { import -> + val count = imports.count { + it.key.pathStr.removeBackticksAndTrim().startsWith( + import.key.pathStr.removeBackticksAndTrim().substringBefore(methodCallExpression) + ) + } + // Parent import and static import both are present + if (count > 1) { + return true + } } - } return false } + private fun ASTNode.containsMethodCall() = text.split(".").last().contains("(") + // Check if the import being checked is present in the filtered import list - private fun isAValidImport(importPath: String): Boolean { - return imports.contains(importPath) - } + private fun isAValidImport(importPath: String) = + imports.any { + it.key.pathStr.removeBackticksAndTrim().contains(importPath) + } private fun String.isComponentN() = componentNRegex.matches(this) @@ -201,5 +232,36 @@ class NoUnusedImportsRule : Rule("no-unused-imports") { return (callOrThis.parent as? KtDotQualifiedExpression)?.takeIf { it.selectorExpression == callOrThis } } - private fun String.removeBackticks() = replace("`", "") + private fun String.removeBackticksAndTrim() = replace("`", "").trim() + + private data class Reference(val text: String, val inDotQualifiedExpression: Boolean) + + private companion object { + val componentNRegex = Regex("^component\\d+$") + + val operatorSet = setOf( + // unary + "unaryPlus", "unaryMinus", "not", + // inc/dec + "inc", "dec", + // arithmetic + "plus", "minus", "times", "div", "rem", "mod", "rangeTo", + // in + "contains", + // indexed access + "get", "set", + // invoke + "invoke", + // augmented assignments + "plusAssign", "minusAssign", "timesAssign", "divAssign", "modAssign", + // (in)equality + "equals", + // comparison + "compareTo", + // iteration (https://github.com/shyiko/ktlint/issues/40) + "iterator", + // by (https://github.com/shyiko/ktlint/issues/54) + "getValue", "setValue" + ) + } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoWildcardImportsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoWildcardImportsRule.kt index eb8f784d1e..99756959a8 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoWildcardImportsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoWildcardImportsRule.kt @@ -1,9 +1,9 @@ package com.pinterest.ktlint.ruleset.standard import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import com.pinterest.ktlint.core.ast.ElementType.IMPORT_DIRECTIVE -import com.pinterest.ktlint.core.ast.isRoot import com.pinterest.ktlint.ruleset.standard.internal.importordering.PatternEntry import org.ec4j.core.model.PropertyType import org.jetbrains.kotlin.com.intellij.lang.ASTNode @@ -12,20 +12,21 @@ import org.jetbrains.kotlin.psi.KtImportDirective public class NoWildcardImportsRule : Rule("no-wildcard-imports"), UsesEditorConfigProperties { - private var allowedWildcardImports: List = emptyList() - override val editorConfigProperties: List> = listOf( packagesToUseImportOnDemandProperty ) - override fun visit( + private lateinit var allowedWildcardImports: List + + override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) { + allowedWildcardImports = editorConfigProperties.getEditorConfigValue(packagesToUseImportOnDemandProperty) + } + + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { - if (node.isRoot()) { - allowedWildcardImports = node.getEditorConfigValue(packagesToUseImportOnDemandProperty) - } if (node.elementType == IMPORT_DIRECTIVE) { val importDirective = node.psi as KtImportDirective val path = importDirective.importPath ?: return diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/PackageNameRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/PackageNameRule.kt index ff76149a2a..43caa16a17 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/PackageNameRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/PackageNameRule.kt @@ -9,7 +9,7 @@ import org.jetbrains.kotlin.psi.KtPackageDirective * https://kotlinlang.org/docs/coding-conventions.html#naming-rules */ class PackageNameRule : Rule("package-name") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ParameterListWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ParameterListWrappingRule.kt index a8254432dc..4bb52f1250 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ParameterListWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/ParameterListWrappingRule.kt @@ -5,6 +5,7 @@ import com.pinterest.ktlint.core.Rule import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.indentSizeProperty import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.indentStyleProperty import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.maxLineLengthProperty +import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import com.pinterest.ktlint.core.ast.ElementType.FUNCTION_LITERAL import com.pinterest.ktlint.core.ast.ElementType.FUNCTION_TYPE @@ -15,27 +16,23 @@ import com.pinterest.ktlint.core.ast.ElementType.RPAR import com.pinterest.ktlint.core.ast.ElementType.TYPE_PARAMETER_LIST import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER_LIST -import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE -import com.pinterest.ktlint.core.ast.children import com.pinterest.ktlint.core.ast.column -import com.pinterest.ktlint.core.ast.isRoot import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline import com.pinterest.ktlint.core.ast.lineIndent import com.pinterest.ktlint.core.ast.prevLeaf import com.pinterest.ktlint.core.ast.prevSibling import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe -import com.pinterest.ktlint.core.ast.visit -import kotlin.math.max 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.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.psi.KtTypeArgumentList +import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType -class ParameterListWrappingRule : +public class ParameterListWrappingRule : Rule("parameter-list-wrapping"), UsesEditorConfigProperties { override val editorConfigProperties: List> = @@ -48,26 +45,41 @@ class ParameterListWrappingRule : private var indentConfig = IndentConfig.DEFAULT_INDENT_CONFIG private var maxLineLength = -1 - override fun visit( + override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) { + indentConfig = IndentConfig( + indentStyle = editorConfigProperties.getEditorConfigValue(indentStyleProperty), + tabWidth = editorConfigProperties.getEditorConfigValue(indentSizeProperty) + ) + maxLineLength = editorConfigProperties.getEditorConfigValue(maxLineLengthProperty) + } + + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { - if (node.isRoot()) { - indentConfig = IndentConfig( - indentStyle = node.getEditorConfigValue(indentStyleProperty), - tabWidth = node.getEditorConfigValue(indentSizeProperty) - ) - maxLineLength = node.getEditorConfigValue(maxLineLengthProperty) - return - } if (indentConfig.disabled) { return } + when (node.elementType) { + NULLABLE_TYPE -> wrapNullableType(node, emit, autoCorrect) + VALUE_PARAMETER_LIST -> { + if (node.needToWrapParameterList()) { + wrapParameterList(node, emit, autoCorrect) + } + } + } + } + + private fun wrapNullableType( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean + ) { + require(node.elementType == NULLABLE_TYPE) node - .takeIf { it.elementType == NULLABLE_TYPE } - ?.takeUnless { + .takeUnless { // skip when max line length not set or does not exceed max line length maxLineLength <= 0 || (node.column - 1 + node.textLength) <= maxLineLength }?.findChildByType(FUNCTION_TYPE) @@ -98,122 +110,121 @@ class ParameterListWrappingRule : } } } + } - if (node.elementType == VALUE_PARAMETER_LIST && - // skip when there are no parameters - node.firstChildNode?.treeNext?.elementType != RPAR && + private fun ASTNode.needToWrapParameterList() = + if ( // skip when there are no parameters + firstChildNode?.treeNext?.elementType != RPAR && // skip lambda parameters - node.treeParent?.elementType != FUNCTION_LITERAL && + treeParent?.elementType != FUNCTION_LITERAL && // skip when function type is wrapped in a nullable type [which was already when processing the nullable // type node itself. - !(node.treeParent.elementType == FUNCTION_TYPE && node.treeParent?.treeParent?.elementType == NULLABLE_TYPE) + !(treeParent.elementType == FUNCTION_TYPE && treeParent?.treeParent?.elementType == NULLABLE_TYPE) ) { // 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.column - 1 + node.textLength) > maxLineLength && !node.textContains('\n') - if (putParametersOnSeparateLines) { - val currentIndentLevel = indentConfig.indentLevelFrom(node.lineIndent()) - val newIndentLevel = - when { - // IDEA quirk: - // fun < - // T, - // R> test( - // param1: T - // param2: R - // ) - // instead of - // fun < - // T, - // R> test( - // param1: T - // param2: R - // ) - currentIndentLevel > 0 && node.hasTypeParameterListInFront() -> currentIndentLevel - 1 - - else -> currentIndentLevel + textContains('\n') || this.exceedsMaxLineLength() + } else { + false + } + + private fun wrapParameterList( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean + ) { + val newIndentLevel = getNewIndentLevel(node) + node + .children() + .forEach { child -> wrapParemeterInList(newIndentLevel, child, emit, autoCorrect) } + } + + private fun getNewIndentLevel(node: ASTNode): Int { + val currentIndentLevel = indentConfig.indentLevelFrom(node.lineIndent()) + return when { + // IDEA quirk: + // fun < + // T, + // R> test( + // param1: T + // param2: R + // ) + // instead of + // fun < + // T, + // R> test( + // param1: T + // param2: R + // ) + currentIndentLevel > 0 && node.hasTypeParameterListInFront() -> currentIndentLevel - 1 + + else -> currentIndentLevel + } + } + + private fun wrapParemeterInList( + newIndentLevel: Int, + child: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean + ) { + val indent = "\n" + indentConfig.indent.repeat(newIndentLevel) + when (child.elementType) { + LPAR -> { + val prevLeaf = child.prevLeaf() + if (prevLeaf is PsiWhiteSpace && prevLeaf.textContains('\n')) { + emit(child.startOffset, errorMessage(child), true) + if (autoCorrect) { + prevLeaf.delete() } - val indent = "\n" + indentConfig.indent.repeat(newIndentLevel) - - nextChild@ for (child in node.children()) { - when (child.elementType) { - LPAR -> { - val prevLeaf = child.prevLeaf() - if (prevLeaf is PsiWhiteSpace && prevLeaf.textContains('\n')) { - emit(child.startOffset, errorMessage(child), true) - if (autoCorrect) { - prevLeaf.delete() - } - } - } - VALUE_PARAMETER, - RPAR -> { - var paramInnerIndentAdjustment = 0 - - // aiming for - // ... LPAR - // VALUE_PARAMETER... - // RPAR - val intendedIndent = if (child.elementType == VALUE_PARAMETER) { - indent + indentConfig.indent - } else { - indent - } + } + } + VALUE_PARAMETER, + RPAR -> { + // aiming for + // ... LPAR + // VALUE_PARAMETER... + // RPAR + val intendedIndent = if (child.elementType == VALUE_PARAMETER) { + indent + indentConfig.indent + } else { + indent + } - val prevLeaf = child.prevLeaf() - if (prevLeaf is PsiWhiteSpace) { - if (prevLeaf.getText().contains("\n")) { - // The current child is already wrapped to a new line. Checking and fixing the - // correct size of the indent is the responsibility of the IndentationRule. - continue@nextChild - } else { - // The current child needs to be wrapped to a newline. - emit(child.startOffset, errorMessage(child), true) - if (autoCorrect) { - // The indentation is purely based on the previous leaf only. Note that in - // autoCorrect mode the indent rule, if enabled, runs after this rule and - // determines the final indentation. But if the indent rule is disabled then the - // indent of this rule is kept. - paramInnerIndentAdjustment = intendedIndent.length - prevLeaf.getTextLength() - (prevLeaf as LeafPsiElement).rawReplaceWithText(intendedIndent) - } - } - } else { - // Insert a new whitespace element in order to wrap the current child to a new line. - emit(child.startOffset, errorMessage(child), true) - if (autoCorrect) { - paramInnerIndentAdjustment = intendedIndent.length - child.column - node.addChild(PsiWhiteSpaceImpl(intendedIndent), child) - } - } - if (paramInnerIndentAdjustment != 0 && child.elementType == VALUE_PARAMETER) { - child.visit { n -> - if (n.elementType == WHITE_SPACE && n.textContains('\n')) { - val split = n.text.split("\n") - (n as LeafElement).rawReplaceWithText( - split.joinToString("\n") { - if (paramInnerIndentAdjustment > 0) { - it + " ".repeat(paramInnerIndentAdjustment) - } else { - it.substring(0, max(it.length + paramInnerIndentAdjustment, 0)) - } - } - ) - } - } - } + val prevLeaf = child.prevLeaf() + if (prevLeaf is PsiWhiteSpace) { + if (prevLeaf.getText().contains("\n")) { + // The current child is already wrapped to a new line. Checking and fixing the + // correct size of the indent is the responsibility of the IndentationRule. + return + } else { + // The current child needs to be wrapped to a newline. + emit(child.startOffset, errorMessage(child), true) + if (autoCorrect) { + // The indentation is purely based on the previous leaf only. Note that in + // autoCorrect mode the indent rule, if enabled, runs after this rule and + // determines the final indentation. But if the indent rule is disabled then the + // indent of this rule is kept. + (prevLeaf as LeafPsiElement).rawReplaceWithText(intendedIndent) } } + } else { + // Insert a new whitespace element in order to wrap the current child to a new line. + emit(child.startOffset, errorMessage(child), true) + if (autoCorrect) { + child.treeParent.addChild(PsiWhiteSpaceImpl(intendedIndent), child) + } } + // Indentation of child nodes need to be fixed by the IndentationRule. } } } + private fun ASTNode.exceedsMaxLineLength() = + maxLineLength > -1 && (column - 1 + textLength) > maxLineLength && !textContains('\n') + private fun errorMessage(node: ASTNode) = when (node.elementType) { LPAR -> """Unnecessary newline before "("""" diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundAngleBracketsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundAngleBracketsRule.kt index bd4a0a3b73..fff9f54cdb 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundAngleBracketsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundAngleBracketsRule.kt @@ -16,7 +16,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement class SpacingAroundAngleBracketsRule : Rule("spacing-around-angle-brackets") { private fun String.trimBeforeLastLine() = this.substring(this.lastIndexOf('\n')) - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundColonRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundColonRule.kt index 2a8510da4d..9d814e2848 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundColonRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundColonRule.kt @@ -28,7 +28,7 @@ import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull class SpacingAroundColonRule : Rule("colon-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundCommaRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundCommaRule.kt index 59d7bae7e5..dc8d02516e 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundCommaRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundCommaRule.kt @@ -21,7 +21,7 @@ class SpacingAroundCommaRule : Rule("comma-spacing") { private val rTokenSet = TokenSet.create(RPAR, RBRACKET, GT) - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundCurlyRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundCurlyRule.kt index 9ef5994c1f..c53895e6e4 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundCurlyRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundCurlyRule.kt @@ -33,7 +33,7 @@ import org.jetbrains.kotlin.psi.KtLambdaExpression class SpacingAroundCurlyRule : Rule("curly-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundDotRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundDotRule.kt index 564d2a40ac..cd4d1a3a84 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundDotRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundDotRule.kt @@ -11,7 +11,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement class SpacingAroundDotRule : Rule("dot-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundDoubleColonRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundDoubleColonRule.kt index ac6ca144cc..6b52778567 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundDoubleColonRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundDoubleColonRule.kt @@ -12,7 +12,7 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement class SpacingAroundDoubleColonRule : Rule("double-colon-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundKeywordRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundKeywordRule.kt index 10d3d85860..8eb691a784 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundKeywordRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundKeywordRule.kt @@ -37,7 +37,7 @@ class SpacingAroundKeywordRule : Rule("keyword-spacing") { private val keywordsWithoutSpaces = create(GET_KEYWORD, SET_KEYWORD) - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundOperatorsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundOperatorsRule.kt index 9bfbc47873..14cb64e522 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundOperatorsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundOperatorsRule.kt @@ -45,7 +45,7 @@ class SpacingAroundOperatorsRule : Rule("op-spacing") { EXCLEQ, ANDAND, OROR, ELVIS, EQ, MULTEQ, DIVEQ, PERCEQ, PLUSEQ, MINUSEQ, ARROW ) - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundParensRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundParensRule.kt index 59f7e6dac7..b31d210782 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundParensRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundParensRule.kt @@ -24,7 +24,7 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace */ class SpacingAroundParensRule : Rule("paren-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundRangeOperatorRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundRangeOperatorRule.kt index e160ebf760..854e507304 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundRangeOperatorRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundRangeOperatorRule.kt @@ -9,7 +9,7 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace class SpacingAroundRangeOperatorRule : Rule("range-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundUnaryOperatorRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundUnaryOperatorRule.kt index 5a0e4a3f5c..8891b06192 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundUnaryOperatorRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingAroundUnaryOperatorRule.kt @@ -13,7 +13,7 @@ import org.jetbrains.kotlin.com.intellij.lang.ASTNode * @see [Kotlin Style Guide](https://kotlinlang.org/docs/reference/coding-conventions.html#horizontal-whitespace) */ class SpacingAroundUnaryOperatorRule : Rule("unary-op-spacing") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingBetweenDeclarationsWithAnnotationsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingBetweenDeclarationsWithAnnotationsRule.kt index 334f934d22..acad724342 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingBetweenDeclarationsWithAnnotationsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingBetweenDeclarationsWithAnnotationsRule.kt @@ -18,7 +18,7 @@ import org.jetbrains.kotlin.psi.psiUtil.prevLeafs * @see https://youtrack.jetbrains.com/issue/KT-35106 */ class SpacingBetweenDeclarationsWithAnnotationsRule : Rule("spacing-between-declarations-with-annotations") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingBetweenDeclarationsWithCommentsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingBetweenDeclarationsWithCommentsRule.kt index 6335114935..e45ed49ba7 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingBetweenDeclarationsWithCommentsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/SpacingBetweenDeclarationsWithCommentsRule.kt @@ -17,7 +17,7 @@ import org.jetbrains.kotlin.psi.psiUtil.startOffset * @see https://youtrack.jetbrains.com/issue/KT-35088 */ class SpacingBetweenDeclarationsWithCommentsRule : Rule("spacing-between-declarations-with-comments") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StringTemplateRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StringTemplateRule.kt index 7b1009d663..9abd6e5380 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StringTemplateRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StringTemplateRule.kt @@ -18,7 +18,7 @@ import org.jetbrains.kotlin.psi.KtThisExpression class StringTemplateRule : Rule("string-template") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaRule.kt index d78c5093ef..54f51b5214 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaRule.kt @@ -1,11 +1,11 @@ package com.pinterest.ktlint.ruleset.experimental.trailingcomma import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import com.pinterest.ktlint.core.ast.ElementType import com.pinterest.ktlint.core.ast.children import com.pinterest.ktlint.core.ast.containsLineBreakInRange -import com.pinterest.ktlint.core.ast.isRoot import com.pinterest.ktlint.core.ast.prevCodeLeaf import com.pinterest.ktlint.core.ast.prevLeaf import kotlin.properties.Delegates @@ -64,25 +64,24 @@ public class TrailingCommaRule : ) ), UsesEditorConfigProperties { - - private var allowTrailingComma by Delegates.notNull() - private var allowTrailingCommaOnCallSite by Delegates.notNull() - override val editorConfigProperties: List> = listOf( allowTrailingCommaProperty, allowTrailingCommaOnCallSiteProperty ) - override fun visit( + private var allowTrailingComma by Delegates.notNull() + private var allowTrailingCommaOnCallSite by Delegates.notNull() + + override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) { + allowTrailingComma = editorConfigProperties.getEditorConfigValue(allowTrailingCommaProperty) + allowTrailingCommaOnCallSite = editorConfigProperties.getEditorConfigValue(allowTrailingCommaOnCallSiteProperty) + } + + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { - if (node.isRoot()) { - allowTrailingComma = node.getEditorConfigValue(allowTrailingCommaProperty) - allowTrailingCommaOnCallSite = node.getEditorConfigValue(allowTrailingCommaOnCallSiteProperty) - } - // Keep processing of element types in sync with Intellij Kotlin formatting settings. // https://github.com/JetBrains/intellij-kotlin/blob/master/formatter/src/org/jetbrains/kotlin/idea/formatter/trailingComma/util.kt when (node.elementType) { diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/WrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/WrappingRule.kt index 8119b0f8c0..b155573f0d 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/WrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/WrappingRule.kt @@ -3,6 +3,7 @@ package com.pinterest.ktlint.ruleset.standard import com.pinterest.ktlint.core.IndentConfig import com.pinterest.ktlint.core.Rule import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties +import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import com.pinterest.ktlint.core.ast.ElementType import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION @@ -50,7 +51,6 @@ import com.pinterest.ktlint.core.ast.prevLeaf import com.pinterest.ktlint.core.ast.prevSibling import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe -import com.pinterest.ktlint.core.ast.visit import com.pinterest.ktlint.core.initKtLintKLogger import mu.KotlinLogging import org.jetbrains.kotlin.com.intellij.lang.ASTNode @@ -67,17 +67,12 @@ private val logger = KotlinLogging.logger {}.initKtLintKLogger() * This rule inserts missing newlines (e.g. between parentheses of a multi-line function call). This logic previously * was part of the IndentationRule (phase 1). * - * Current limitations: - * - "all or nothing" (currently, rule can only be disabled for an entire file) - * - Whenever a linebreak is inserted, this rules assumes that the parent node it indented correctly. So the indentation - * is fixed with respect to indentation of the parent. This is just a simple best effort for the case that the - * indentation rule is not run. + * Whenever a linebreak is inserted, this rules assumes that the parent node it indented correctly. So the indentation + * is fixed with respect to indentation of the parent. This is just a simple best effort for the case that the + * indentation rule is not run. */ public class WrappingRule : - Rule( - id = "wrapping", - visitorModifiers = setOf(VisitorModifier.RunOnRootNodeOnly) - ), + Rule("wrapping"), UsesEditorConfigProperties { override val editorConfigProperties: List> = listOf( @@ -85,37 +80,29 @@ public class WrappingRule : DefaultEditorConfigProperties.indentStyleProperty ) - private companion object { - private val lTokenSet = TokenSet.create(LPAR, LBRACE, LBRACKET, LT) - private val rTokenSet = TokenSet.create(RPAR, RBRACE, RBRACKET, GT) - private val matchingRToken = - lTokenSet.types.zip( - rTokenSet.types - ).toMap() - } - private var line = 1 private lateinit var indentConfig: IndentConfig - override fun visit( + override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) { + line = 1 + indentConfig = IndentConfig( + indentStyle = editorConfigProperties.getEditorConfigValue(DefaultEditorConfigProperties.indentStyleProperty), + tabWidth = editorConfigProperties.getEditorConfigValue(DefaultEditorConfigProperties.indentSizeProperty) + ) + } + + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { - line = 1 - indentConfig = IndentConfig( - indentStyle = node.getEditorConfigValue(DefaultEditorConfigProperties.indentStyleProperty), - tabWidth = node.getEditorConfigValue(DefaultEditorConfigProperties.indentSizeProperty) - ) - node.visit { n -> // TODO: Check whether this visit can be removed like other rules. This would disabling the rule for blocks and lines - when (n.elementType) { - LPAR, LBRACE, LBRACKET -> rearrangeBlock(n, autoCorrect, emit) // TODO: LT - SUPER_TYPE_LIST -> rearrangeSuperTypeList(n, autoCorrect, emit) - VALUE_PARAMETER_LIST, VALUE_ARGUMENT_LIST -> rearrangeValueList(n, autoCorrect, emit) - ARROW -> rearrangeArrow(n, autoCorrect, emit) - WHITE_SPACE -> line += n.text.count { it == '\n' } - CLOSING_QUOTE -> rearrangeClosingQuote(n, autoCorrect, emit) - } + when (node.elementType) { + LPAR, LBRACE, LBRACKET -> rearrangeBlock(node, autoCorrect, emit) // TODO: LT + SUPER_TYPE_LIST -> rearrangeSuperTypeList(node, autoCorrect, emit) + VALUE_PARAMETER_LIST, VALUE_ARGUMENT_LIST -> rearrangeValueList(node, autoCorrect, emit) + ARROW -> rearrangeArrow(node, autoCorrect, emit) + WHITE_SPACE -> line += node.text.count { it == '\n' } + CLOSING_QUOTE -> rearrangeClosingQuote(node, autoCorrect, emit) } } @@ -506,4 +493,13 @@ public class WrappingRule : } return true } + + private companion object { + private val lTokenSet = TokenSet.create(LPAR, LBRACE, LBRACKET, LT) + private val rTokenSet = TokenSet.create(RPAR, RBRACE, RBRACKET, GT) + private val matchingRToken = + lTokenSet.types.zip( + rTokenSet.types + ).toMap() + } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/ArgumentListWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/ArgumentListWrappingRuleTest.kt index 9397ee085f..c0d97d5bd1 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/ArgumentListWrappingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/ArgumentListWrappingRuleTest.kt @@ -197,6 +197,7 @@ class ArgumentListWrappingRuleTest { ) """.trimIndent() argumentListWrappingRuleAssertThat(code) + .addAdditionalRules(IndentationRule()) .hasLintViolations( LintViolation(3, 18, "Argument should be on a separate line (unless all arguments can fit a single line)"), LintViolation(5, 6, "Missing newline before \")\""), @@ -354,6 +355,7 @@ class ArgumentListWrappingRuleTest { } """.trimIndent() argumentListWrappingRuleAssertThat(code) + .addAdditionalRules(IndentationRule()) .hasLintViolations( LintViolation(4, 21, "Argument should be on a separate line (unless all arguments can fit a single line)"), LintViolation(7, 10, "Missing newline before \")\"") diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt index 8d95801da6..330c03667a 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt @@ -3909,6 +3909,46 @@ internal class IndentationRuleTest { } } + @Nested + inner class SuppressionInMiddleOfFile { + @Test + fun `Issue 631 - Given some code for which indentation is disabled with ktlint-disable-enable-block then do not fix indentation of that block only`() { + val code = + """ + val fooWithIndentationFixing1: String = + "foo" + + "bar" + // ktlint-disable indent + val fooWithIndentationFixingSuppressed: String = + "foo" + + "bar" + // ktlint-enable indent + val fooWithIndentationFixing2: String = + "foo" + + "bar" + """.trimIndent() + indentationRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Issue 631 - Given some code for which indentation is disabled with @Suppress on an element then do not fix indentation of that element only`() { + val code = + """ + val fooWithIndentationFixing1: String = + "foo" + + "bar" + @Suppress("ktlint:indent") + val fooWithIndentationFixingSuppressed: String = + "foo" + + "bar" + val fooWithIndentationFixing2: String = + "foo" + + "bar" + """.trimIndent() + indentationRuleAssertThat(code).hasNoLintViolations() + } + } + private companion object { val INDENT_STYLE_TAB = indentStyleProperty to PropertyType.IndentStyleValue.tab } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/NoUnusedImportsRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/NoUnusedImportsRuleTest.kt index b9f2065102..ac2c5f0c2b 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/NoUnusedImportsRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/NoUnusedImportsRuleTest.kt @@ -397,7 +397,9 @@ class NoUnusedImportsRuleTest { package com.example import org.mockito.Mockito + import org.mockito.Mockito.mock1 import org.mockito.Mockito.withSettings + fun foo() { Mockito.mock(String::class.java, Mockito.withSettings().defaultAnswer { }) } @@ -407,13 +409,17 @@ class NoUnusedImportsRuleTest { package com.example import org.mockito.Mockito + fun foo() { Mockito.mock(String::class.java, Mockito.withSettings().defaultAnswer { }) } """.trimIndent() noUnusedImportsRuleAssertThat(code) - .hasLintViolation(4, 1, "Unused import") - .isFormattedAs(formattedCode) +// .hasLintViolation(4, 1, "Unused import") + .hasLintViolations( + LintViolation(4, 1, "Unused import"), + LintViolation(5, 1, "Unused import") + ).isFormattedAs(formattedCode) } @Test diff --git a/ktlint-ruleset-template/src/main/kotlin/yourpkgname/NoVarRule.kt b/ktlint-ruleset-template/src/main/kotlin/yourpkgname/NoVarRule.kt index d1c24dc758..6872d9c57c 100644 --- a/ktlint-ruleset-template/src/main/kotlin/yourpkgname/NoVarRule.kt +++ b/ktlint-ruleset-template/src/main/kotlin/yourpkgname/NoVarRule.kt @@ -6,7 +6,7 @@ import org.jetbrains.kotlin.com.intellij.lang.ASTNode class NoVarRule : Rule("no-var") { - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit diff --git a/ktlint-ruleset-test/src/main/kotlin/com/pinterest/ruleset/test/DumpASTRule.kt b/ktlint-ruleset-test/src/main/kotlin/com/pinterest/ruleset/test/DumpASTRule.kt index a04f9cc49d..17afab7045 100644 --- a/ktlint-ruleset-test/src/main/kotlin/com/pinterest/ruleset/test/DumpASTRule.kt +++ b/ktlint-ruleset-test/src/main/kotlin/com/pinterest/ruleset/test/DumpASTRule.kt @@ -25,7 +25,7 @@ public class DumpASTRule @JvmOverloads constructor( private var lineNumberColumnLength: Int = 0 private var lastNode: ASTNode? = null - override fun visit( + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, corrected: Boolean) -> Unit