Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
0949748
commit df43622
Showing
81 changed files
with
1,999 additions
and
1,584 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
271 changes: 90 additions & 181 deletions
271
ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/PreparedCode.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.