Skip to content

Commit

Permalink
Add support for API Consumers to add suppressions (#2428)
Browse files Browse the repository at this point in the history
* Extract KtlintSuppression from KtlintSuppressionRule

KtlintSuppression is able to insert or modify a 'Suppress' annotation for a given node. It has no knowledge of the legacy ktlint-directives. The KtlintSuppressionRule handles the legacy ktlint-directives and delegates insertion/modification of the 'Suppress' annotation to the KtlintSuppression helper.

KtlintSuppressionRule:
* Fix offsets in some violations
* Allow annotation to be placed on a value argument, value parameter, type projection or type parameter
  • Loading branch information
paul-dingemans authored Dec 11, 2023
1 parent 8b51fb9 commit ad879b8
Show file tree
Hide file tree
Showing 10 changed files with 1,616 additions and 423 deletions.
2 changes: 2 additions & 0 deletions ktlint-rule-engine-core/api/ktlint-rule-engine-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ public final class com/pinterest/ktlint/rule/engine/core/api/ASTNodeExtensionKt
public static synthetic fun prevLeaf$default (Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;ZILjava/lang/Object;)Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;
public static final fun prevSibling (Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;
public static synthetic fun prevSibling$default (Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;
public static final fun remove (Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;)V
public static final fun replaceWith (Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;)V
public static final fun upsertWhitespaceAfterMe (Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;Ljava/lang/String;)V
public static final fun upsertWhitespaceBeforeMe (Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;Ljava/lang/String;)V
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,3 +481,12 @@ public fun ASTNode.betweenCodeSiblings(
afterElementType: IElementType,
beforeElementType: IElementType,
): Boolean = afterCodeSibling(afterElementType) && beforeCodeSibling(beforeElementType)

public fun ASTNode.replaceWith(node: ASTNode) {
treeParent.addChild(node, this)
this.remove()
}

public fun ASTNode.remove() {
treeParent.removeChild(this)
}
31 changes: 31 additions & 0 deletions ktlint-rule-engine/api/ktlint-rule-engine.api
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,37 @@ public final class com/pinterest/ktlint/rule/engine/api/KtLintRuleException : ja
public final fun getRuleId ()Ljava/lang/String;
}

public final class com/pinterest/ktlint/rule/engine/api/KtlintRuleEngineSuppressionKt {
public static final fun insertSuppression (Lcom/pinterest/ktlint/rule/engine/api/KtLintRuleEngine;Lcom/pinterest/ktlint/rule/engine/api/Code;Lcom/pinterest/ktlint/rule/engine/api/KtlintSuppression;)Ljava/lang/String;
}

public abstract class com/pinterest/ktlint/rule/engine/api/KtlintSuppression {
public synthetic fun <init> (Lcom/pinterest/ktlint/rule/engine/core/api/RuleId;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getRuleId ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId;
}

public final class com/pinterest/ktlint/rule/engine/api/KtlintSuppressionAtOffset : com/pinterest/ktlint/rule/engine/api/KtlintSuppression {
public fun <init> (IILcom/pinterest/ktlint/rule/engine/core/api/RuleId;)V
public final fun getCol ()I
public final fun getLine ()I
}

public abstract class com/pinterest/ktlint/rule/engine/api/KtlintSuppressionException : java/lang/RuntimeException {
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}

public final class com/pinterest/ktlint/rule/engine/api/KtlintSuppressionForFile : com/pinterest/ktlint/rule/engine/api/KtlintSuppression {
public fun <init> (Lcom/pinterest/ktlint/rule/engine/core/api/RuleId;)V
}

public final class com/pinterest/ktlint/rule/engine/api/KtlintSuppressionNoElementFoundException : com/pinterest/ktlint/rule/engine/api/KtlintSuppressionException {
public fun <init> (Lcom/pinterest/ktlint/rule/engine/api/KtlintSuppressionAtOffset;)V
}

public final class com/pinterest/ktlint/rule/engine/api/KtlintSuppressionOutOfBoundsException : com/pinterest/ktlint/rule/engine/api/KtlintSuppressionException {
public fun <init> (Lcom/pinterest/ktlint/rule/engine/api/KtlintSuppressionAtOffset;)V
}

public final class com/pinterest/ktlint/rule/engine/api/LintError {
public fun <init> (IILcom/pinterest/ktlint/rule/engine/core/api/RuleId;Ljava/lang/String;Z)V
public fun equals (Ljava/lang/Object;)Z
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.pinterest.ktlint.rule.engine.api

import com.pinterest.ktlint.rule.engine.core.api.RuleId
import com.pinterest.ktlint.rule.engine.internal.RuleExecutionContext
import com.pinterest.ktlint.rule.engine.internal.insertKtlintRuleSuppression
import org.jetbrains.kotlin.com.intellij.lang.ASTNode

/**
* A [Suppress] annotation can only be inserted at specific locations. This function is intended for API Consumers. It updates given [code]
* by inserting a [Suppress] annotation for the given [suppression].
*
* Throws [KtlintSuppressionOutOfBoundsException] when the position of the [suppression] can not be found in the [code]. Throws
* [KtlintSuppressionNoElementFoundException] when no element can be found at the given offset.
*
* Returns the code with the inserted/modified suppression. Note that the returned code may not (yet) comply with formatting of all rules.
* This is intentional as adding a suppression for the [suppression] does not mean that other lint errors which can be autocorrected should
* be autocorrected.
*/
public fun KtLintRuleEngine.insertSuppression(
code: Code,
suppression: KtlintSuppression,
): String {
val rootNode =
RuleExecutionContext
.createRuleExecutionContext(this, code)
.rootNode

rootNode
.findLeafElementAt(suppression)
.insertKtlintRuleSuppression(setOf(suppression.ruleId.value))

return rootNode.text
}

private fun ASTNode.findLeafElementAt(suppression: KtlintSuppression): ASTNode =
when (suppression) {
is KtlintSuppressionForFile -> this

is KtlintSuppressionAtOffset ->
findLeafElementAt(suppression.offsetFromStartOf(text))
?: throw KtlintSuppressionNoElementFoundException(suppression)
}

private fun KtlintSuppressionAtOffset.offsetFromStartOf(code: String): Int {
if (line < 1 || col < 1) {
throw KtlintSuppressionOutOfBoundsException(this)
}

val lines = code.split("\n")

if (line > lines.size) {
throw KtlintSuppressionOutOfBoundsException(this)
}
val startOffsetOfLineContainingLintError =
lines
.take((line - 1).coerceAtLeast(0))
.sumOf { text ->
// Fix length for newlines which were removed while splitting the original code
text.length + 1
}

val codeLine = lines[line - 1]
if (col > codeLine.length) {
throw KtlintSuppressionOutOfBoundsException(this)
}

return startOffsetOfLineContainingLintError + (col - 1)
}

public sealed class KtlintSuppressionException(
message: String,
) : RuntimeException(message)

public class KtlintSuppressionOutOfBoundsException(
offsetSuppression: KtlintSuppressionAtOffset,
) : KtlintSuppressionException("Offset (${offsetSuppression.line},${offsetSuppression.col}) is invalid")

public class KtlintSuppressionNoElementFoundException(
offsetSuppression: KtlintSuppressionAtOffset,
) : KtlintSuppressionException("No ASTNode found at offset (${offsetSuppression.line},${offsetSuppression.col})")

public sealed class KtlintSuppression(
public val ruleId: RuleId,
)

public class KtlintSuppressionForFile(
ruleId: RuleId,
) : KtlintSuppression(ruleId)

public class KtlintSuppressionAtOffset(
public val line: Int,
public val col: Int,
ruleId: RuleId,
) : KtlintSuppression(ruleId)
Loading

0 comments on commit ad879b8

Please sign in to comment.