Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade ktlint to 0.47.1 #5312

Merged
merged 12 commits into from Oct 7, 2022
Expand Up @@ -30,8 +30,11 @@ internal fun PsiElement.searchName(): String {
* Example: KtFile with name /full/path/to/Test.kt will have its name formatted to be simply Test.kt
*/
private fun String.formatElementName(): String =
if (contains(File.separatorChar)) substringAfterLast(File.separatorChar)
else this
if (contains(File.separatorChar)) {
substringAfterLast(File.separatorChar)
} else {
this
}
Comment on lines +33 to +37
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This violation is detected once pinterest/ktlint#828 is addressed.


/*
* KtCompiler wrongly used Path.filename as the name for a KtFile instead of the whole path.
Expand Down
Expand Up @@ -79,15 +79,19 @@ class AnnotationExcluderSpec(private val env: KotlinCoreEnvironment) {
@Test
fun `should exclude when the annotation was found with SplitPattern`() {
val (file, ktAnnotation) = createKtFile("@SinceKotlin")
val excluder = @Suppress("DEPRECATION") AnnotationExcluder(file, SplitPattern("SinceKotlin"))

@Suppress("DEPRECATION")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annotation with parameter(s) should be placed on a separate line prior to the annotated construct [AnnotationOnSeparateLine]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These seems like a false positive from ktlint, isn't it? They are forcing you to increase the annotation scope.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notes taken: I will investigate this later and open an issue for ktlint.

val excluder = AnnotationExcluder(file, SplitPattern("SinceKotlin"))

assertThat(excluder.shouldExclude(listOf(ktAnnotation))).isTrue()
}

@Test
fun `should exclude when the annotation was found with List of Strings`() {
val (file, ktAnnotation) = createKtFile("@SinceKotlin")
val excluder = @Suppress("DEPRECATION") AnnotationExcluder(file, listOf("SinceKo*"))

@Suppress("DEPRECATION")
val excluder = AnnotationExcluder(file, listOf("SinceKo*"))

assertThat(excluder.shouldExclude(listOf(ktAnnotation))).isTrue()
}
Expand Down
Expand Up @@ -2,8 +2,8 @@ package io.gitlab.arturbosch.detekt.formatting

import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.core.Rule.VisitorModifier.RunAsLateAsPossible
import com.pinterest.ktlint.core.Rule.VisitorModifier.RunOnRootNodeOnly
import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.codeStyleSetProperty
import com.pinterest.ktlint.core.api.EditorConfigProperties
import com.pinterest.ktlint.core.api.UsesEditorConfigProperties
import io.github.detekt.psi.fileName
import io.github.detekt.psi.toFilePath
Expand All @@ -21,9 +21,9 @@ import io.gitlab.arturbosch.detekt.api.SourceLocation
import io.gitlab.arturbosch.detekt.api.TextLocation
import org.ec4j.core.model.Property
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.lang.FileASTNode
import org.jetbrains.kotlin.com.intellij.psi.impl.source.JavaDummyElement
import org.jetbrains.kotlin.com.intellij.psi.impl.source.JavaDummyHolder
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.psiUtil.endOffset

/**
* Rule to detect formatting violations.
Expand All @@ -39,12 +39,35 @@ abstract class FormattingRule(config: Config) : Rule(config) {
protected val isAndroid
get() = FormattingProvider.android.value(ruleSetConfig)

val runOnRootNodeOnly
get() = RunOnRootNodeOnly in wrapping.visitorModifiers

val runAsLateAsPossible
get() = RunAsLateAsPossible in wrapping.visitorModifiers

private val emit = { offset: Int, message: String, _: Boolean ->
val (line, column) = positionByOffset(offset)
val location = Location(
SourceLocation(line, column),
// Use offset + 1 since ktlint always reports a single location.
TextLocation(offset, offset + 1),
root.toFilePath()
)

// Nodes reported by 'NoConsecutiveBlankLines' are dangling whitespace nodes which means they have
// no direct parent which we can use to get the containing file needed to baseline or suppress findings.
// For these reasons we do not report a KtElement which may lead to crashes when postprocessing it
// e.g. reports (html), baseline etc.
val packageName = root.packageFqName.asString()
.takeIf { it.isNotEmpty() }
?.plus(".")
.orEmpty()
val entity = Entity("", "$packageName${root.fileName}:$line", location, root)

if (canBeCorrectedByKtLint(message)) {
report(CorrectableCodeSmell(issue, entity, message, autoCorrectEnabled = autoCorrect))
} else {
report(CodeSmell(issue, entity, message))
}
}

private var positionByOffset: (offset: Int) -> Pair<Int, Int> by SingleAssign()
private var root: KtFile by SingleAssign()

Expand All @@ -58,66 +81,63 @@ abstract class FormattingRule(config: Config) : Rule(config) {
this.root = root
positionByOffset = KtLintLineColCalculator
.calculateLineColByOffset(KtLintLineColCalculator.normalizeText(root.text))
root.node.putUserData(KtLint.FILE_PATH_USER_DATA_KEY, root.name)

val editorConfigProperties = overrideEditorConfigProperties()?.toMutableMap()
wrapping.beforeFirstNode(computeEditorConfigProperties())
root.node.visitASTNodes()
wrapping.afterLastNode()
Comment on lines +86 to +88
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See Rule lifecycle hooks / deprecate RunOnRootOnly visitor modifier in https://github.com/pinterest/ktlint/releases/tag/0.47.0

}

open fun overrideEditorConfigProperties(): Map<UsesEditorConfigProperties.EditorConfigProperty<*>, String>? = null

private fun computeEditorConfigProperties(): EditorConfigProperties {
val usesEditorConfigProperties = overrideEditorConfigProperties()?.toMutableMap()
?: mutableMapOf()

if (isAndroid) {
editorConfigProperties[codeStyleSetProperty] = "android"
usesEditorConfigProperties[codeStyleSetProperty] = "android"
}

if (editorConfigProperties.isNotEmpty()) {
val userData = (root.node.getUserData(KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY).orEmpty())
.toMutableMap()

editorConfigProperties.forEach { (editorConfigProperty, defaultValue) ->
userData[editorConfigProperty.type.name] = Property.builder()
.name(editorConfigProperty.type.name)
.type(editorConfigProperty.type)
.value(defaultValue)
.build()
return buildMap {
usesEditorConfigProperties.forEach { (editorConfigProperty, defaultValue) ->
put(
key = editorConfigProperty.type.name,
value = Property.builder()
.name(editorConfigProperty.type.name)
.type(editorConfigProperty.type)
.value(defaultValue)
.build()
)
}
root.node.putUserData(KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, userData)
}
root.node.putUserData(KtLint.FILE_PATH_USER_DATA_KEY, root.name)
}

open fun overrideEditorConfigProperties(): Map<UsesEditorConfigProperties.EditorConfigProperty<*>, String>? = null

fun apply(node: ASTNode) {
if (ruleShouldOnlyRunOnFileNode(node)) {
return
private fun beforeVisitChildNodes(node: ASTNode) {
wrapping.beforeVisitChildNodes(node, autoCorrect) { offset, errorMessage, canBeAutoCorrected ->
emit.invoke(offset, errorMessage, canBeAutoCorrected)
}
}

wrapping.visit(node, autoCorrect) { offset, message, _ ->
val (line, column) = positionByOffset(offset)
val location = Location(
SourceLocation(line, column),
getTextLocationForViolation(node, offset),
root.toFilePath()
)

// Nodes reported by 'NoConsecutiveBlankLines' are dangling whitespace nodes which means they have
// no direct parent which we can use to get the containing file needed to baseline or suppress findings.
// For these reasons we do not report a KtElement which may lead to crashes when postprocessing it
// e.g. reports (html), baseline etc.
val packageName = root.packageFqName.asString()
.takeIf { it.isNotEmpty() }
?.plus(".")
.orEmpty()
val entity = Entity("", "$packageName${root.fileName}:$line", location, root)

if (canBeCorrectedByKtLint(message)) {
report(CorrectableCodeSmell(issue, entity, message, autoCorrectEnabled = autoCorrect))
} else {
report(CodeSmell(issue, entity, message))
}
private fun afterVisitChildNodes(node: ASTNode) {
wrapping.afterVisitChildNodes(node, autoCorrect) { offset, errorMessage, canBeAutoCorrected ->
emit.invoke(offset, errorMessage, canBeAutoCorrected)
}
}

open fun getTextLocationForViolation(node: ASTNode, offset: Int) =
TextLocation(node.startOffset, node.psi.endOffset)
Comment on lines -118 to -119
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is removed because no rules in Ktlint is RunOnRootNodeOnly. RunOnRootNodeOnly itself is also deprecated and to be removed in 0.48.0

private fun ASTNode.visitASTNodes() {
if (isNotDummyElement()) {
beforeVisitChildNodes(this)
}
getChildren(null).forEach {
it.visitASTNodes()
}
if (isNotDummyElement()) {
afterVisitChildNodes(this)
}
}

private fun ruleShouldOnlyRunOnFileNode(node: ASTNode) =
runOnRootNodeOnly && node !is FileASTNode
private fun ASTNode.isNotDummyElement(): Boolean {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function is renamed to improve readability and consistency

val parent = this.psi?.parent
return parent !is JavaDummyHolder && parent !is JavaDummyElement
}
}
Expand Up @@ -57,14 +57,12 @@ import io.gitlab.arturbosch.detekt.formatting.wrappers.SpacingBetweenDeclaration
import io.gitlab.arturbosch.detekt.formatting.wrappers.SpacingBetweenDeclarationsWithComments
import io.gitlab.arturbosch.detekt.formatting.wrappers.SpacingBetweenFunctionNameAndOpeningParenthesis
import io.gitlab.arturbosch.detekt.formatting.wrappers.StringTemplate
import io.gitlab.arturbosch.detekt.formatting.wrappers.TrailingComma
import io.gitlab.arturbosch.detekt.formatting.wrappers.TrailingCommaOnCallSite
import io.gitlab.arturbosch.detekt.formatting.wrappers.TrailingCommaOnDeclarationSite
import io.gitlab.arturbosch.detekt.formatting.wrappers.TypeArgumentListSpacing
import io.gitlab.arturbosch.detekt.formatting.wrappers.TypeParameterListSpacing
import io.gitlab.arturbosch.detekt.formatting.wrappers.UnnecessaryParenthesesBeforeTrailingLambda
import io.gitlab.arturbosch.detekt.formatting.wrappers.Wrapping
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.impl.source.JavaDummyElement
import org.jetbrains.kotlin.com.intellij.psi.impl.source.JavaDummyHolder
import org.jetbrains.kotlin.psi.KtFile
import java.util.LinkedList

Expand Down Expand Up @@ -119,7 +117,8 @@ class KtLintMultiRule(config: Config = Config.empty) :
SpacingBetweenDeclarationsWithAnnotations(config),
SpacingBetweenDeclarationsWithComments(config),
StringTemplate(config),
TrailingComma(config), // in standard ruleset but not enabled by default
TrailingCommaOnCallSite(config), // in standard ruleset but not enabled by default
TrailingCommaOnDeclarationSite(config), // in standard ruleset but not enabled by default
Comment on lines +120 to +121
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TrailingCommaRule is split into two separate rules - pinterest/ktlint#1555

I would propose to keep them disabled because it may sparkle endless conversation within a medium-large sized team

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't they be enforced by default on declaration site in line with the Kotlin coding conventions?

https://kotlinlang.org/docs/coding-conventions.html#trailing-commas

The Kotlin style guide encourages the use of trailing commas at the declaration site

I also think they should be enabled by default for the call site for the reasons outlined in the coding conventions:

  • It makes version-control diffs cleaner – as all the focus is on the changed value.
  • It makes it easy to add and reorder elements – there is no need to add or delete the comma if you manipulate elements.

It's easy to disable for teams that don't want this. I don't think there's a strong reason to deviate from ktlint's default behaviour - and anyone using ktlint directly would have to have the same conversation if they don't like them. It's also easy to disable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's a strong reason to deviate from ktlint's default behaviour

Agree on being consistent with KtLint's behavior

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me keep this disabled for now in a separate PR because it will cause at least 200+ violations across the codebase - I will create a separate PR to demonstrate what it looks like.

Copy link
Member Author

@chao2zhang chao2zhang Oct 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#5386 is the PR to be consistent with ktlint

Shouldn't they be enforced by default on declaration site in line with the Kotlin coding conventions?
Yes, and ktlint's default configuration still diverges from kotlin coding convention. We could configure these rules in a way that conforms to the kotlin coding convention, namely:

  • TrailingCommaOnDeclarationSite: active = true, useTrailingCommaOnDeclarationSite = true
  • TrailingCommaOnDeclarationSite: active = false

Wrapping(config),

// Wrappers for ktlint-ruleset-experimental rules. Disabled by default.
Expand All @@ -142,43 +141,23 @@ class KtLintMultiRule(config: Config = Config.empty) :
)

override fun visit(root: KtFile) {
val sortedRules = getSortedRules()
sortedRules.forEach { it.visit(root) }
root.node.visitTokens { node ->
sortedRules.forEach { it.apply(node) }
getSortedRules().forEach { rule ->
rule.visit(root)
Comment on lines +144 to +145
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am trying to move more logic into FormattingRule itself to prepare for #5192 (comment)

}
}

internal fun getSortedRules(): List<FormattingRule> {
val runFirstOnRoot = mutableListOf<FormattingRule>()
val other = mutableListOf<FormattingRule>()
val runLastOnRoot = mutableListOf<FormattingRule>()
val runLast = mutableListOf<FormattingRule>()
for (rule in activeRules.filterIsInstance<FormattingRule>()) {
when {
rule.runOnRootNodeOnly && rule.runAsLateAsPossible -> runLastOnRoot.add(rule)
rule.runOnRootNodeOnly -> runFirstOnRoot.add(rule)
rule.runAsLateAsPossible -> runLast.add(rule)
else -> other.add(rule)
}
}
return LinkedList<FormattingRule>().apply {
addAll(runFirstOnRoot)
addAll(other)
addAll(runLastOnRoot)
addAll(runLast)
}
}

private fun ASTNode.visitTokens(currentNode: (ASTNode) -> Unit) {
if (this.isNoFakeElement()) {
currentNode(this)
}
getChildren(null).forEach { it.visitTokens(currentNode) }
}

private fun ASTNode.isNoFakeElement(): Boolean {
val parent = this.psi?.parent
return parent !is JavaDummyHolder && parent !is JavaDummyElement
}
}
Expand Up @@ -4,13 +4,11 @@ import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties
import com.pinterest.ktlint.core.api.UsesEditorConfigProperties
import com.pinterest.ktlint.ruleset.standard.IndentationRule
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.TextLocation
import io.gitlab.arturbosch.detekt.api.config
import io.gitlab.arturbosch.detekt.api.internal.ActiveByDefault
import io.gitlab.arturbosch.detekt.api.internal.AutoCorrectable
import io.gitlab.arturbosch.detekt.api.internal.Configuration
import io.gitlab.arturbosch.detekt.formatting.FormattingRule
import org.jetbrains.kotlin.com.intellij.lang.ASTNode

/**
* See [ktlint docs](https://pinterest.github.io/ktlint/rules/standard/#indentation) for documentation.
Expand All @@ -36,15 +34,4 @@ class Indentation(config: Config) : FormattingRule(config) {
mapOf(
DefaultEditorConfigProperties.indentSizeProperty to indentSize.toString(),
)

/**
* [IndentationRule] has visitor modifier RunOnRootNodeOnly, so [node] is always the root file.
* Override the parent implementation to highlight the entire file.
*/
override fun getTextLocationForViolation(node: ASTNode, offset: Int): TextLocation {
val relativeEnd = node.text
.drop(offset)
.indexOfFirst { !it.isWhitespace() }
return TextLocation(offset, offset + relativeEnd)
}
}
@@ -0,0 +1,27 @@
package io.gitlab.arturbosch.detekt.formatting.wrappers

import com.pinterest.ktlint.core.api.UsesEditorConfigProperties
import com.pinterest.ktlint.ruleset.standard.TrailingCommaOnCallSiteRule
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.config
import io.gitlab.arturbosch.detekt.api.internal.AutoCorrectable
import io.gitlab.arturbosch.detekt.api.internal.Configuration
import io.gitlab.arturbosch.detekt.formatting.FormattingRule

/**
* See [ktlint docs](https://pinterest.github.io/ktlint/rules/standard/) for documentation.
*/
@AutoCorrectable(since = "1.22.0")
class TrailingCommaOnCallSite(config: Config) : FormattingRule(config) {

override val wrapping = TrailingCommaOnCallSiteRule()
override val issue = issueFor("Rule to mandate/forbid trailing commas at call sites")

@Configuration("Defines whether a trailing comma (or no trailing comma) should be enforced at call sites")
private val useTrailingCommaOnCallSite by config(false)

override fun overrideEditorConfigProperties(): Map<UsesEditorConfigProperties.EditorConfigProperty<*>, String> =
mapOf(
TrailingCommaOnCallSiteRule.allowTrailingCommaOnCallSiteProperty to useTrailingCommaOnCallSite.toString(),
)
}
@@ -1,7 +1,7 @@
package io.gitlab.arturbosch.detekt.formatting.wrappers

import com.pinterest.ktlint.core.api.UsesEditorConfigProperties
import com.pinterest.ktlint.ruleset.experimental.trailingcomma.TrailingCommaRule
import com.pinterest.ktlint.ruleset.standard.TrailingCommaOnDeclarationSiteRule
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.config
import io.gitlab.arturbosch.detekt.api.internal.AutoCorrectable
Expand All @@ -11,21 +11,18 @@ import io.gitlab.arturbosch.detekt.formatting.FormattingRule
/**
* See [ktlint docs](https://pinterest.github.io/ktlint/rules/standard/) for documentation.
*/
@AutoCorrectable(since = "1.20.0")
class TrailingComma(config: Config) : FormattingRule(config) {
@AutoCorrectable(since = "1.22.0")
class TrailingCommaOnDeclarationSite(config: Config) : FormattingRule(config) {

override val wrapping = TrailingCommaRule()
override val issue = issueFor("Rule to mandate/forbid trailing commas")
override val wrapping = TrailingCommaOnDeclarationSiteRule()
override val issue = issueFor("Rule to mandate/forbid trailing commas at declaration sites")

@Configuration("Defines whether a trailing comma (or no trailing comma) should be enforced on the defining side")
private val allowTrailingComma by config(false)

@Configuration("Defines whether a trailing comma (or no trailing comma) should be enforced on the calling side")
private val allowTrailingCommaOnCallSite by config(false)
@Configuration("Defines whether a trailing comma (or no trailing comma) should be enforced at declaration sites")
private val useTrailingCommaOnDeclarationSite by config(false)

override fun overrideEditorConfigProperties(): Map<UsesEditorConfigProperties.EditorConfigProperty<*>, String> =
mapOf(
TrailingCommaRule.allowTrailingCommaProperty to allowTrailingComma.toString(),
TrailingCommaRule.allowTrailingCommaOnCallSiteProperty to allowTrailingCommaOnCallSite.toString(),
TrailingCommaOnDeclarationSiteRule.allowTrailingCommaProperty to
useTrailingCommaOnDeclarationSite.toString(),
)
}