diff --git a/CHANGELOG.md b/CHANGELOG.md index 18caaa55d2..bdbdb9d7fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -280,6 +280,8 @@ Previously the default value for `.editorconfig` property `max_line_length` was * Wrap annotated function parameters to a separate line in code style `ktlint_official` only. `function-signature`, `parameter-list-wrapping` ([#1908](https://github.com/pinterest/ktlint/issues/1908)) * Wrap annotated projection types in type argument lists to a separate line `annotation` ([#1909](https://github.com/pinterest/ktlint/issues/1909)) * Add newline after adding trailing comma in parameter list of a function literal `trailing-comma-on-declaration-site` ([#1911](https://github.com/pinterest/ktlint/issues/1911)) +* Wrap annotations before class constructor in code style `ktlint_official`. `annotation` ([#1916](https://github.com/pinterest/ktlint/issues/1916)) +* Annotations on type projections should be wrapped in same way as other annotations `annotation` ([#1917](https://github.com/pinterest/ktlint/issues/1917)) ### Changed * Wrap the parameters of a function literal containing a multiline parameter list (only in `ktlint_official` code style) `parameter-list-wrapping` ([#1681](https://github.com/pinterest/ktlint/issues/1681)). diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRule.kt index bdddca0835..7dbe0f6a16 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRule.kt @@ -1,47 +1,47 @@ package com.pinterest.ktlint.ruleset.standard.rules -import com.pinterest.ktlint.rule.engine.core.api.ElementType import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATED_EXPRESSION import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATION import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATION_ENTRY +import com.pinterest.ktlint.rule.engine.core.api.ElementType.BLOCK +import com.pinterest.ktlint.rule.engine.core.api.ElementType.CONSTRUCTOR_KEYWORD import com.pinterest.ktlint.rule.engine.core.api.ElementType.FILE_ANNOTATION_LIST +import com.pinterest.ktlint.rule.engine.core.api.ElementType.GT import com.pinterest.ktlint.rule.engine.core.api.ElementType.MODIFIER_LIST +import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_ARGUMENT_LIST import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_PROJECTION import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_REFERENCE import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT_LIST import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER -import com.pinterest.ktlint.rule.engine.core.api.ElementType.WHITE_SPACE import com.pinterest.ktlint.rule.engine.core.api.IndentConfig import com.pinterest.ktlint.rule.engine.core.api.Rule.VisitorModifier.RunAfterRule import com.pinterest.ktlint.rule.engine.core.api.Rule.VisitorModifier.RunAfterRule.Mode.REGARDLESS_WHETHER_RUN_AFTER_RULE_IS_LOADED_OR_DISABLED import com.pinterest.ktlint.rule.engine.core.api.RuleId import com.pinterest.ktlint.rule.engine.core.api.children import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.firstChildLeafOrSelf -import com.pinterest.ktlint.rule.engine.core.api.isCodeLeaf +import com.pinterest.ktlint.rule.engine.core.api.indent import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline import com.pinterest.ktlint.rule.engine.core.api.lastChildLeafOrSelf import com.pinterest.ktlint.rule.engine.core.api.nextCodeLeaf -import com.pinterest.ktlint.rule.engine.core.api.nextLeaf -import com.pinterest.ktlint.rule.engine.core.api.prevCodeLeaf +import com.pinterest.ktlint.rule.engine.core.api.nextCodeSibling +import com.pinterest.ktlint.rule.engine.core.api.nextSibling import com.pinterest.ktlint.rule.engine.core.api.prevLeaf import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceAfterMe import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceBeforeMe import com.pinterest.ktlint.rule.engine.core.util.safeAs import com.pinterest.ktlint.ruleset.standard.StandardRule import org.jetbrains.kotlin.com.intellij.lang.ASTNode -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.descriptors.annotations.AnnotationUseSiteTarget import org.jetbrains.kotlin.psi.KtAnnotationEntry -import org.jetbrains.kotlin.psi.psiUtil.leaves import org.jetbrains.kotlin.psi.psiUtil.siblings -import org.jetbrains.kotlin.utils.addToStdlib.applyIf +import org.jetbrains.kotlin.utils.addToStdlib.ifTrue /** * Ensures that annotation are wrapped to separate lines. @@ -107,8 +107,12 @@ public class AnnotationRule : ) { when (node.elementType) { FILE_ANNOTATION_LIST -> { + visitAnnotationList(node, emit, autoCorrect) visitFileAnnotationList(node, emit, autoCorrect) } + ANNOTATED_EXPRESSION, MODIFIER_LIST -> { + visitAnnotationList(node, emit, autoCorrect) + } ANNOTATION -> { // Annotation array // @[...] @@ -117,116 +121,186 @@ public class AnnotationRule : ANNOTATION_ENTRY -> { visitAnnotationEntry(node, emit, autoCorrect) } + TYPE_ARGUMENT_LIST -> { + visitTypeArgumentList(node, emit, autoCorrect) + } } } - private fun visitAnnotationEntry( + private fun visitAnnotationList( node: ASTNode, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, autoCorrect: Boolean, ) { - require(node.elementType == ANNOTATION_ENTRY) + require(node.elementType in ANNOTATION_CONTAINER) + + if (node.shouldWrapAnnotations()) { + val expectedIndent = + when { + node.elementType == ANNOTATED_EXPRESSION -> + node.treeParent.indent().plus(indentConfig.indent) + node.hasAnnotationBeforeConstructor() -> + node.treeParent.treeParent.indent().plus(indentConfig.indent) + else -> + node.treeParent.indent() + } - if (node.isAnnotationEntryWithValueArgumentList() && - node.treeParent.treeParent.elementType != VALUE_PARAMETER && - node.treeParent.treeParent.elementType != VALUE_ARGUMENT && - node.isNotReceiverTargetAnnotation() - ) { - // Disallow: - // @Foo class Bar - // Allow function parameters and arguments to have annotation on same line as identifier: - // fun @receiver:Bar String.foo() {} - // fun foo( - // @Bar("bar") bar, - // @Bar("bar") "42", - // ) {} - checkForAnnotationWithParameterToBePlacedOnSeparateLine(node, emit, autoCorrect) - } + node + .children() + .filter { it.elementType == ANNOTATION_ENTRY } + .filter { + it.isAnnotationEntryWithValueArgumentList() || + !it.isPrecededByOtherAnnotationEntryWithoutParametersOnTheSameLine() + }.forEach { annotationEntry -> + annotationEntry + .prevLeaf() + ?.let { prevLeaf -> + // Let the indentation rule determine the exact indentation and only report and fix when the line needs to be + // wrapped + if (!prevLeaf.textContains('\n')) { + emit(prevLeaf.startOffset, "Expected newline before annotation", true) + if (autoCorrect) { + prevLeaf.upsertWhitespaceBeforeMe( + prevLeaf + .text + .substringBeforeLast('\n', "") + .plus(expectedIndent), + ) + } + } + } + } - if (node.isOnSameLineAsAnnotatedConstruct()) { - if (node.isPrecededByAnnotationOnAnotherLine()) { - // Code below is disallowed - // @Foo1 - // @Foo2 fun foo() {} - emit( - node.startOffset, - "Annotation must be placed on a separate line when it is preceded by another annotation on a separate line", - true, - ) - if (autoCorrect) { - node - .lastChildLeafOrSelf() - .nextLeaf() - ?.upsertWhitespaceBeforeMe(getNewlineWithIndent(node.treeParent)) + node + .children() + .last { it.elementType == ANNOTATION_ENTRY } + .lastChildLeafOrSelf() + .nextCodeLeaf() + ?.prevLeaf() + ?.let { prevLeaf -> + // Let the indentation rule determine the exact indentation and only report and fix when the line needs to be wrapped + if (!prevLeaf.textContains('\n')) { + emit(prevLeaf.startOffset, "Expected newline after last annotation", true) + if (autoCorrect) { + prevLeaf.upsertWhitespaceAfterMe(expectedIndent) + } + } + } + + node + .takeIf { it.elementType == ANNOTATED_EXPRESSION } + ?.treeParent + ?.takeIf { it.elementType == BLOCK } + ?.nextSibling() + ?.let { nextSibling -> + // Let the indentation rule determine the exact indentation and only report and fix when the line needs to be wrapped + if (!nextSibling.textContains('\n')) { + emit(nextSibling.startOffset, "Expected newline", true) + if (autoCorrect) { + nextSibling.upsertWhitespaceBeforeMe(node.indent()) + } + } } + } + } + + private fun ASTNode.shouldWrapAnnotations() = + hasAnnotationWithParameter() || + hasMultipleAnnotationsOnSameLine() || + hasAnnotationBeforeConstructor() + + private fun ASTNode.hasAnnotationWithParameter(): Boolean { + require(elementType in ANNOTATION_CONTAINER) + return children() + .any { + it.isAnnotationEntryWithValueArgumentList() && + it.treeParent.treeParent.elementType != VALUE_PARAMETER && + it.treeParent.treeParent.elementType != VALUE_ARGUMENT && + it.isNotReceiverTargetAnnotation() } + } - if (node.treeParent.elementType != ANNOTATION && - node.treeParent.treeParent.elementType != VALUE_PARAMETER && - node.treeParent.treeParent.elementType != VALUE_ARGUMENT && - node.isPrecededByOtherAnnotationEntryOnTheSameLine() && - node.isLastAnnotationEntry() - ) { + private fun ASTNode.hasMultipleAnnotationsOnSameLine(): Boolean { + require(elementType in ANNOTATION_CONTAINER) + return children() + .any { + it.treeParent.elementType != ANNOTATION && + it.treeParent.treeParent.elementType != VALUE_PARAMETER && + it.treeParent.treeParent.elementType != VALUE_ARGUMENT && + it.isPrecededByOtherAnnotationEntryOnTheSameLine() && + it.isLastAnnotationEntry() // Code below is disallowed // @Foo1 @Foo2 fun foo() {} // But following is allowed: // @[Foo1 Foo2] fun foo() {} // fun foo(@Bar1 @Bar2 bar) {} - emit( - node.findAnnotatedConstruct().startOffset, - "Multiple annotations should not be placed on the same line as the annotated construct", - true, - ) - if (autoCorrect) { - node - .lastChildLeafOrSelf() - .nextCodeLeaf() - ?.upsertWhitespaceBeforeMe( - getNewlineWithIndent(node.treeParent) - .applyIf(node.typeProjectionOrNull() != null) { - plus(indentConfig.indent) - }, - ) - } } + } - node.typeProjectionOrNull() - ?.prevCodeLeaf() - ?.let { startOfList -> - // Code below is disallowed - // var foo: List<@Foo1 @Foo2 String> - // But following is allowed: - // var foo: List<@[Foo1 Foo2] String> - // fun foo(@Bar1 @Bar2 bar) {} - if (node.isFollowedByOtherAnnotationEntryOnTheSameLine() && - node.isFirstAnnotationEntry() - ) { - emit(startOfList.startOffset, "Expected newline after '${startOfList.text}'", true) - if (autoCorrect) { - startOfList - .upsertWhitespaceAfterMe( - getNewlineWithIndent(node.treeParent).plus(indentConfig.indent), - ) + private fun ASTNode.hasAnnotationBeforeConstructor() = + codeStyle == CodeStyleValue.ktlint_official && + hasAnnotationEntry() && + nextCodeSibling()?.elementType == CONSTRUCTOR_KEYWORD + + private fun ASTNode.hasAnnotationEntry() = children().any { it.elementType == ANNOTATION_ENTRY } + + private fun visitTypeArgumentList( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean, + ) { + node + .children() + .filter { it.elementType == TYPE_PROJECTION } + .mapNotNull { it.findChildByType(TYPE_REFERENCE) } + .filter { it.elementType == TYPE_REFERENCE } + .mapNotNull { it.findChildByType(MODIFIER_LIST) } + .filter { it.elementType == MODIFIER_LIST } + .any { it.shouldWrapAnnotations() } + .ifTrue { wrapTypeArgumentList(node, emit, autoCorrect) } + } + + private fun wrapTypeArgumentList( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean, + ) { + val expectedIndent = node.indent().plus(indentConfig.indent) + + node + .children() + .filter { it.elementType == TYPE_PROJECTION } + .forEach { typeProjection -> + typeProjection + .prevLeaf() + ?.let { prevLeaf -> + if (prevLeaf.text != expectedIndent) { + emit(prevLeaf.startOffset, "Expected newline", true) + if (autoCorrect) { + prevLeaf.upsertWhitespaceAfterMe(expectedIndent) + } } } - if (node.isPrecededByOtherAnnotationEntryOnTheSameLine() && - node.isLastAnnotationEntry() - ) { - node - .findAnnotatedConstruct() - .treeParent - .lastChildLeafOrSelf() - .nextLeaf { it.isCodeLeaf() && it.elementType != ElementType.COMMA } - ?.let { codeLeaf -> - emit(codeLeaf.startOffset, "Expected newline before '${codeLeaf.text}'", true) - if (autoCorrect) { - codeLeaf.upsertWhitespaceBeforeMe(getNewlineWithIndent(node.treeParent)) - } - } + } + + node + .findChildByType(GT) + ?.let { gt -> + if (gt.prevLeaf()?.text != expectedIndent) { + emit(gt.startOffset, "Expected newline", true) + if (autoCorrect) { + gt.upsertWhitespaceBeforeMe(expectedIndent) } } - } + } + } + private fun visitAnnotationEntry( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean, + ) { + require(node.elementType == ANNOTATION_ENTRY) if (node.isPrecededByOtherAnnotationEntryOnTheSameLine() && node.isPrecededByAnnotationOnAnotherLine()) { // Code below is disallowed // @Foo1 @@ -245,15 +319,6 @@ public class AnnotationRule : } } - private fun ASTNode.typeProjectionOrNull() = - takeIf { elementType == ANNOTATION_ENTRY } - ?.takeIf { it.treeParent.elementType == MODIFIER_LIST } - ?.treeParent - ?.takeIf { it.treeParent.elementType == TYPE_REFERENCE } - ?.treeParent - ?.takeIf { it.treeParent.elementType == TYPE_PROJECTION } - ?.treeParent - private fun ASTNode.isPrecededByAnnotationOnAnotherLine(): Boolean { val firstAnnotation = treeParent.findChildByType(ANNOTATION_ENTRY) return siblings(forward = false) @@ -261,52 +326,6 @@ public class AnnotationRule : .any { it.isWhiteSpaceWithNewline() } } - private fun checkForAnnotationWithParameterToBePlacedOnSeparateLine( - node: ASTNode, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - autoCorrect: Boolean, - ) { - if (node.isPrecededByOtherAnnotationEntry() && node.isOnSameLineAsPreviousAnnotationEntry()) { - emit( - node.startOffset, - "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct", - true, - ) - if (autoCorrect) { - node - .firstChildLeafOrSelf() - .upsertWhitespaceBeforeMe(" ") - } - } - - if (node.isOnSameLineAsNextAnnotationEntryOrAnnotatedConstruct()) { - emit( - node.startOffset, - "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct", - // Annotated expressions for which the annotation contains a parameter can be hard to correct - // automatically. See examples below. For now, let them be fixed manually. - // fun foo1() = @Suppress("DEPRECATION") bar() - // if (@Suppress("DEPRECATION") bar()) { .. } - node.treeParent.elementType != ANNOTATED_EXPRESSION, - ) - if (autoCorrect) { - node - .lastChildLeafOrSelf() - .nextLeaf() - .safeAs() - ?.let { - if (it.elementType == WHITE_SPACE) { - it.replaceWithText(getNewlineWithIndent(node.treeParent)) - } else { - it.rawInsertBeforeMe( - PsiWhiteSpaceImpl(getNewlineWithIndent(node.treeParent)), - ) - } - } - } - } - } - private fun ASTNode.isNotReceiverTargetAnnotation() = getAnnotationUseSiteTarget() != AnnotationUseSiteTarget.RECEIVER private fun ASTNode.getAnnotationUseSiteTarget() = @@ -321,25 +340,19 @@ public class AnnotationRule : takeIf { it.elementType == ANNOTATION_ENTRY } ?.findChildByType(VALUE_ARGUMENT_LIST) - private fun ASTNode.isFirstAnnotationEntry() = - this == - treeParent - .children() - .firstOrNull { it.elementType == ANNOTATION_ENTRY } - private fun ASTNode.isLastAnnotationEntry() = this == treeParent .children() .lastOrNull { it.elementType == ANNOTATION_ENTRY } - private fun ASTNode.isPrecededByOtherAnnotationEntryOnTheSameLine() = + private fun ASTNode.isPrecededByOtherAnnotationEntryWithoutParametersOnTheSameLine() = siblings(forward = false) - .takeWhile { !it.isWhiteSpaceWithNewline() } - .any { it.elementType == ANNOTATION_ENTRY } + .takeWhile { !it.isWhiteSpaceWithNewline() && !it.isAnnotationEntryWithValueArgumentList() } + .any { it.elementType == ANNOTATION_ENTRY && !it.isAnnotationEntryWithValueArgumentList() } - private fun ASTNode.isFollowedByOtherAnnotationEntryOnTheSameLine() = - siblings() + private fun ASTNode.isPrecededByOtherAnnotationEntryOnTheSameLine() = + siblings(forward = false) .takeWhile { !it.isWhiteSpaceWithNewline() } .any { it.elementType == ANNOTATION_ENTRY } @@ -359,33 +372,6 @@ public class AnnotationRule : .takeWhile { it.elementType != ANNOTATION_ENTRY } .none { it.isWhiteSpaceWithNewline() } - private fun ASTNode.isOnSameLineAsAnnotatedConstruct(): Boolean { - val annotatedConstruct = findAnnotatedConstruct() - return lastChildLeafOrSelf() - .leaves(forward = true) - .takeWhile { it != annotatedConstruct } - .none { it.isWhiteSpaceWithNewline() } - } - - private fun ASTNode.findAnnotatedConstruct(): ASTNode { - val astNode = - if (treeParent.elementType == MODIFIER_LIST) { - treeParent - } else { - this - } - return checkNotNull( - astNode.lastChildLeafOrSelf().nextCodeLeaf(), - ) - } - - private fun ASTNode.isOnSameLineAsNextAnnotationEntryOrAnnotatedConstruct() = - if (isFollowedByOtherAnnotationEntry()) { - isOnSameLineAsNextAnnotationEntry() - } else { - isOnSameLineAsAnnotatedConstruct() - } - private fun visitFileAnnotationList( node: ASTNode, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, @@ -448,6 +434,15 @@ public class AnnotationRule : .substringAfterLast('\n') return "\n".plus(indentWithoutNewline) } + + private companion object { + val ANNOTATION_CONTAINER = + listOf( + ANNOTATED_EXPRESSION, + FILE_ANNOTATION_LIST, + MODIFIER_LIST, + ) + } } public val ANNOTATION_RULE_ID: RuleId = AnnotationRule().ruleId diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt index 50f8b7f213..b91c6b6bb2 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRule.kt @@ -48,6 +48,7 @@ import com.pinterest.ktlint.rule.engine.core.api.ElementType.OBJECT_DECLARATION import com.pinterest.ktlint.rule.engine.core.api.ElementType.OPEN_QUOTE import com.pinterest.ktlint.rule.engine.core.api.ElementType.OPERATION_REFERENCE import com.pinterest.ktlint.rule.engine.core.api.ElementType.PARENTHESIZED +import com.pinterest.ktlint.rule.engine.core.api.ElementType.PRIMARY_CONSTRUCTOR import com.pinterest.ktlint.rule.engine.core.api.ElementType.PROPERTY import com.pinterest.ktlint.rule.engine.core.api.ElementType.PROPERTY_ACCESSOR import com.pinterest.ktlint.rule.engine.core.api.ElementType.RBRACE @@ -577,14 +578,24 @@ public class IndentationRule : toAstNode = typeConstraintList.lastChildLeafOrSelf(), ).prevCodeLeaf() } - node - .findChildByType(SUPER_TYPE_LIST) - ?.let { superTypeList -> - nextToAstNode = startIndentContext( - fromAstNode = superTypeList.getPrecedingLeadingCommentsAndWhitespaces(), - toAstNode = superTypeList.lastChildLeafOrSelf(), - ).prevCodeLeaf() - } + + val primaryConstructor = node.findChildByType(PRIMARY_CONSTRUCTOR) + if (codeStyle == ktlint_official && primaryConstructor != null) { + // Indent both constructor and super type list + nextToAstNode = startIndentContext( + fromAstNode = primaryConstructor.getPrecedingLeadingCommentsAndWhitespaces(), + toAstNode = nextToAstNode, + ).prevCodeLeaf() + } else { + node + .findChildByType(SUPER_TYPE_LIST) + ?.let { superTypeList -> + nextToAstNode = startIndentContext( + fromAstNode = superTypeList.getPrecedingLeadingCommentsAndWhitespaces(), + toAstNode = superTypeList.lastChildLeafOrSelf(), + ).prevCodeLeaf() + } + } // Leading annotations and comments should be indented at same level as class itself startIndentContext( diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeArgumentListSpacingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeArgumentListSpacingRule.kt index 54ceba78e5..7cef327593 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeArgumentListSpacingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeArgumentListSpacingRule.kt @@ -1,23 +1,48 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.ElementType +import com.pinterest.ktlint.rule.engine.core.api.IndentConfig import com.pinterest.ktlint.rule.engine.core.api.Rule import com.pinterest.ktlint.rule.engine.core.api.RuleId +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.findCompositeParentElementOfType +import com.pinterest.ktlint.rule.engine.core.api.indent import com.pinterest.ktlint.rule.engine.core.api.isPartOfCompositeElementOfType +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace import com.pinterest.ktlint.rule.engine.core.api.nextLeaf import com.pinterest.ktlint.rule.engine.core.api.nextSibling import com.pinterest.ktlint.rule.engine.core.api.prevLeaf import com.pinterest.ktlint.rule.engine.core.api.prevSibling +import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceAfterMe +import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceBeforeMe import com.pinterest.ktlint.ruleset.standard.StandardRule import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.utils.addToStdlib.applyIf /** * Lints and formats the spacing before and after the angle brackets of a type argument list. */ public class TypeArgumentListSpacingRule : - StandardRule("type-argument-list-spacing"), + StandardRule( + id = "type-argument-list-spacing", + usesEditorConfigProperties = + setOf( + INDENT_SIZE_PROPERTY, + INDENT_STYLE_PROPERTY, + ), + ), Rule.Experimental { + private var indentConfig = IndentConfig.DEFAULT_INDENT_CONFIG + + override fun beforeFirstNode(editorConfig: EditorConfig) { + indentConfig = IndentConfig( + indentStyle = editorConfig[INDENT_STYLE_PROPERTY], + tabWidth = editorConfig[INDENT_SIZE_PROPERTY], + ) + } + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, @@ -70,21 +95,53 @@ public class TypeArgumentListSpacingRule : autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, ) { - // No whitespace expected after opening angle bracket of type argument list - // val list = listOf< String>() + val multiline = node.textContains('\n') + val expectedIndent = + node + .indent() + .applyIf(multiline) { + plus(indentConfig.indent) + } + node .findChildByType(ElementType.LT) ?.nextSibling() - ?.takeIf { it.elementType == ElementType.WHITE_SPACE } - ?.let { noWhitespaceExpected(it, autoCorrect, emit) } + ?.let { nextSibling -> + if (multiline) { + if (nextSibling.text != expectedIndent) { + emit(nextSibling.startOffset, "Expected newline", true) + if (autoCorrect) { + nextSibling.upsertWhitespaceAfterMe(expectedIndent) + } + } + } else { + if (nextSibling.isWhiteSpace()) { + // Disallow + // val list = listOf< String>() + noWhitespaceExpected(nextSibling, autoCorrect, emit) + } + } + } - // No whitespace expected before closing angle bracket of type argument list - // val list = listOf() node .findChildByType(ElementType.GT) ?.prevSibling() - ?.takeIf { it.elementType == ElementType.WHITE_SPACE } - ?.let { noWhitespaceExpected(it, autoCorrect, emit) } + ?.let { prevSibling -> + if (multiline) { + if (prevSibling.text != expectedIndent) { + emit(prevSibling.startOffset, "Expected newline", true) + if (autoCorrect) { + prevSibling.upsertWhitespaceBeforeMe(expectedIndent) + } + } + } else { + if (prevSibling.isWhiteSpace()) { + // Disallow + // val list = listOf() + noWhitespaceExpected(prevSibling, autoCorrect, emit) + } + } + } } private fun noWhitespaceExpected( diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRuleTest.kt index 57c1aa1ef4..c7d12357c7 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRuleTest.kt @@ -1,5 +1,7 @@ package com.pinterest.ktlint.ruleset.standard.rules +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue.ktlint_official import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule import com.pinterest.ktlint.test.LintViolation import org.junit.jupiter.api.Nested @@ -39,11 +41,11 @@ class AnnotationRuleTest { fun `Given an annotation with a parameter followed by a EOL comment`() { val code = """ - @Suppress("AnnotationRule") // some comment + @Suppress("Something") // some comment class FooBar { - @Suppress("AnnotationRule") // some comment + @Suppress("Something") // some comment var foo: String - @Suppress("AnnotationRule") // some comment + @Suppress("Something") // some comment fun bar() {} } """.trimIndent() @@ -54,54 +56,54 @@ class AnnotationRuleTest { fun `Given an annotation with a parameter on same line as annotation construct (possibly separated by a block comment or KDoc)`() { val code = """ - @Suppress("AnnotationRule") class FooBar1 { - @Suppress("AnnotationRule") var foo: String - @Suppress("AnnotationRule") fun bar() {} + @Suppress("Something") class FooBar1 { + @Suppress("Something") var foo: String + @Suppress("Something") fun bar() {} } - @Suppress("AnnotationRule") /* some comment */ class FooBar2 { - @Suppress("AnnotationRule") /* some comment */ var foo: String - @Suppress("AnnotationRule") /* some comment */ fun bar() {} + @Suppress("Something") /* some comment */ class FooBar2 { + @Suppress("Something") /* some comment */ var foo: String + @Suppress("Something") /* some comment */ fun bar() {} } - @Suppress("AnnotationRule") /** some comment */ class FooBar3 { - @Suppress("AnnotationRule") /** some comment */ var foo: String - @Suppress("AnnotationRule") /** some comment */ fun bar() {} + @Suppress("Something") /** some comment */ class FooBar3 { + @Suppress("Something") /** some comment */ var foo: String + @Suppress("Something") /** some comment */ fun bar() {} } """.trimIndent() val formattedCode = """ - @Suppress("AnnotationRule") + @Suppress("Something") class FooBar1 { - @Suppress("AnnotationRule") + @Suppress("Something") var foo: String - @Suppress("AnnotationRule") + @Suppress("Something") fun bar() {} } - @Suppress("AnnotationRule") - /* some comment */ class FooBar2 { - @Suppress("AnnotationRule") - /* some comment */ var foo: String - @Suppress("AnnotationRule") - /* some comment */ fun bar() {} + @Suppress("Something") /* some comment */ + class FooBar2 { + @Suppress("Something") /* some comment */ + var foo: String + @Suppress("Something") /* some comment */ + fun bar() {} } - @Suppress("AnnotationRule") - /** some comment */ class FooBar3 { - @Suppress("AnnotationRule") - /** some comment */ var foo: String - @Suppress("AnnotationRule") - /** some comment */ fun bar() {} + @Suppress("Something") /** some comment */ + class FooBar3 { + @Suppress("Something") /** some comment */ + var foo: String + @Suppress("Something") /** some comment */ + fun bar() {} } """.trimIndent() annotationRuleAssertThat(code) .hasLintViolations( - LintViolation(1, 1, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct"), - LintViolation(2, 5, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct"), - LintViolation(3, 5, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct"), - LintViolation(5, 1, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct"), - LintViolation(6, 5, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct"), - LintViolation(7, 5, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct"), - LintViolation(9, 1, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct"), - LintViolation(10, 5, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct"), - LintViolation(11, 5, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct"), + LintViolation(1, 23, "Expected newline after last annotation"), + LintViolation(2, 27, "Expected newline after last annotation"), + LintViolation(3, 27, "Expected newline after last annotation"), + LintViolation(5, 42, "Expected newline after last annotation"), + LintViolation(6, 46, "Expected newline after last annotation"), + LintViolation(7, 46, "Expected newline after last annotation"), + LintViolation(9, 43, "Expected newline after last annotation"), + LintViolation(10, 47, "Expected newline after last annotation"), + LintViolation(11, 47, "Expected newline after last annotation"), ).isFormattedAs(formattedCode) } @@ -126,9 +128,9 @@ class AnnotationRuleTest { """.trimIndent() annotationRuleAssertThat(code) .hasLintViolations( - LintViolation(1, 11, "Multiple annotations should not be placed on the same line as the annotated construct"), - LintViolation(2, 15, "Multiple annotations should not be placed on the same line as the annotated construct"), - LintViolation(3, 15, "Multiple annotations should not be placed on the same line as the annotated construct"), + LintViolation(1, 10, "Expected newline after last annotation"), + LintViolation(2, 14, "Expected newline after last annotation"), + LintViolation(3, 14, "Expected newline after last annotation"), ).isFormattedAs(formattedCode) } @@ -157,7 +159,7 @@ class AnnotationRuleTest { var foo: String """.trimIndent() annotationRuleAssertThat(code) - .hasLintViolation(1, 21, "Multiple annotations should not be placed on the same line as the annotated construct") + .hasLintViolation(1, 20, "Expected newline after last annotation") .isFormattedAs(formattedCode) } @@ -173,7 +175,7 @@ class AnnotationRuleTest { var foo: String """.trimIndent() annotationRuleAssertThat(code) - .hasLintViolation(1, 21, "Multiple annotations should not be placed on the same line as the annotated construct") + .hasLintViolation(1, 20, "Expected newline after last annotation") .isFormattedAs(formattedCode) } @@ -181,26 +183,26 @@ class AnnotationRuleTest { fun `Given an annotation with a parameter not followed by a space but on same line as annotated construct`() { val code = """ - @Suppress("AnnotationRule")class FooBar { - @Suppress("AnnotationRule")var foo: String - @Suppress("AnnotationRule")fun bar() {} + @Suppress("Something")class FooBar { + @Suppress("Something")var foo: String + @Suppress("Something")fun bar() {} } """.trimIndent() val formattedCode = """ - @Suppress("AnnotationRule") + @Suppress("Something") class FooBar { - @Suppress("AnnotationRule") + @Suppress("Something") var foo: String - @Suppress("AnnotationRule") + @Suppress("Something") fun bar() {} } """.trimIndent() annotationRuleAssertThat(code) .hasLintViolations( - LintViolation(1, 1, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct"), - LintViolation(2, 5, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct"), - LintViolation(3, 5, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct"), + LintViolation(1, 22, "Expected newline after last annotation"), + LintViolation(2, 26, "Expected newline after last annotation"), + LintViolation(3, 26, "Expected newline after last annotation"), ).isFormattedAs(formattedCode) } @@ -230,7 +232,7 @@ class AnnotationRuleTest { } """.trimIndent() annotationRuleAssertThat(code) - .hasLintViolation(2, 5, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct") + .hasLintViolation(7, 6, "Expected newline after last annotation") .isFormattedAs(formattedCode) } @@ -253,7 +255,7 @@ class AnnotationRuleTest { """.trimIndent() @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") annotationRuleAssertThat(code) - .hasLintViolation(3, 5, "Annotation must be placed on a separate line when it is preceded by another annotation on a separate line") + .hasLintViolation(3, 9, "Expected newline after last annotation") .isFormattedAs(formattedCode) } @@ -287,15 +289,6 @@ class AnnotationRuleTest { annotationRuleAssertThat(code).hasNoLintViolations() } - @Test - fun `Issue 628 - Given an annotation before the primary constructor `() { - val code = - """ - class Foo @Inject internal constructor() - """.trimIndent() - annotationRuleAssertThat(code).hasNoLintViolations() - } - @Test fun `Issue 642 - Given annotations on method parameters on same line as parameter`() { val code = @@ -390,6 +383,17 @@ class AnnotationRuleTest { .isFormattedAs(formattedCode) } + @Test + fun `Given a file annotation with parameter followed by a blank line`() { + val code = + """ + @file:JvmName("FooClass") + + package foo.bar + """.trimIndent() + annotationRuleAssertThat(code).hasNoLintViolations() + } + @Test fun `Issue 624 - Given a file annotation with parameter on same line as package`() { val code = @@ -405,7 +409,7 @@ class AnnotationRuleTest { @Suppress("ktlint:argument-list-wrapping", "ktlint:max-line-length") annotationRuleAssertThat(code) .hasLintViolations( - LintViolation(1, 1, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct"), + LintViolation(1, 26, "Expected newline after last annotation"), LintViolation(1, 27, "File annotations should be separated from file contents with a blank line"), ).isFormattedAs(formattedCode) } @@ -501,7 +505,7 @@ class AnnotationRuleTest { fun `Issue 1539 - Given an annotation with parameter followed by an EOL comment and followed by another annotation`() { val code = """ - @Suppress("AnnotationRule") // some comment + @Suppress("Something") // some comment @Bar class Foo """.trimIndent() @@ -512,7 +516,7 @@ class AnnotationRuleTest { fun `Issue 1539 - Given an annotation with parameter followed by an EOL comment on separate line before annotated construct`() { val code = """ - @Suppress("AnnotationRule") + @Suppress("Something") // some comment between last annotation and annotated construct class Foo """.trimIndent() @@ -552,17 +556,45 @@ class AnnotationRuleTest { } @Test - fun `Given an annotated expression on same line as annotated construct and the annotation contains a parameter then report a violation which can not be autocorrected`() { + fun `Given an annotated expression on same line as annotated construct and the annotation contains a parameter`() { val code = """ - fun foo() = @Suppress("DEPRECATION") bar() + fun foo() = @Bar1 @Bar2 @Bar3("bar3") @Bar4 bar() + """.trimIndent() + val formattedCode = + """ + fun foo() = + @Bar1 @Bar2 + @Bar3("bar3") + @Bar4 + bar() """.trimIndent() annotationRuleAssertThat(code) - .hasLintViolationWithoutAutoCorrect( - 1, - 13, - "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct", - ) + .hasLintViolations( + LintViolation(1, 12, "Expected newline before annotation"), + LintViolation(1, 24, "Expected newline before annotation"), + LintViolation(1, 38, "Expected newline before annotation"), + LintViolation(1, 44, "Expected newline after last annotation"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given an annotated expression on same line as annotated construct and no annotation contains a parameter`() { + val code = + """ + fun foo() = @Bar1 @Bar2 bar() + """.trimIndent() + val formattedCode = + """ + fun foo() = + @Bar1 @Bar2 + bar() + """.trimIndent() + annotationRuleAssertThat(code) + .hasLintViolations( + LintViolation(1, 12, "Expected newline before annotation"), + LintViolation(1, 24, "Expected newline after last annotation"), + ).isFormattedAs(formattedCode) } @Test @@ -617,6 +649,27 @@ class AnnotationRuleTest { } } + @Test + fun `Given an annotation and other modifiers before the annotated construct`() { + val code = + """ + @Bar("bar") public class Foo + @Bar("bar") public fun foo() {} + """.trimIndent() + val formattedCode = + """ + @Bar("bar") + public class Foo + @Bar("bar") + public fun foo() {} + """.trimIndent() + annotationRuleAssertThat(code) + .hasLintViolations( + LintViolation(1, 12, "Expected newline after last annotation"), + LintViolation(2, 12, "Expected newline after last annotation"), + ).isFormattedAs(formattedCode) + } + @Test fun `Given a single annotation on same line as a type parameter then do not report a violation`() { val code = @@ -632,6 +685,17 @@ class AnnotationRuleTest { .hasNoLintViolations() } + @Test + fun `Given an annotation with parameter on followed by another modifier on the next line then do not report a violation`() { + val code = + """ + @Target(AnnotationTarget.TYPE) + annotation class Foo + """.trimIndent() + annotationRuleAssertThat(code) + .hasNoLintViolations() + } + @Nested inner class `Issue 1725 - Given multiple annotations on same line as a type parameter` { @Test @@ -645,15 +709,16 @@ class AnnotationRuleTest { val fooBar: List< @Foo @Bar String - > = emptyList() + > = emptyList() """.trimIndent() annotationRuleAssertThat(code) .addAdditionalRuleProvider { TrailingCommaOnDeclarationSiteRule() } .addAdditionalRuleProvider { WrappingRule() } .hasLintViolations( - LintViolation(1, 17, "Expected newline after '<'"), - LintViolation(1, 28, "Multiple annotations should not be placed on the same line as the annotated construct"), - LintViolation(1, 34, "Expected newline before '>'"), + LintViolation(1, 17, "Expected newline"), + LintViolation(1, 17, "Expected newline before annotation"), + LintViolation(1, 27, "Expected newline after last annotation"), + LintViolation(1, 34, "Expected newline"), ).isFormattedAs(formattedCode) } @@ -680,13 +745,16 @@ class AnnotationRuleTest { .addAdditionalRuleProvider { IndentationRule() } .addAdditionalRuleProvider { WrappingRule() } .hasLintViolations( - LintViolation(1, 39, "Expected newline after ','"), - LintViolation(1, 51, "Multiple annotations should not be placed on the same line as the annotated construct"), - LintViolation(1, 57, "Expected newline after ','"), - LintViolation(1, 59, "Expected newline before '@'"), - LintViolation(1, 59, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct"), - LintViolation(1, 76, "Multiple annotations should not be placed on the same line as the annotated construct"), - LintViolation(1, 82, "Expected newline before '>'"), + LintViolation(1, 19, "Expected newline"), + LintViolation(1, 27, "Expected newline"), + LintViolation(1, 40, "Expected newline"), + LintViolation(1, 40, "Expected newline before annotation"), + LintViolation(1, 50, "Expected newline after last annotation"), + LintViolation(1, 58, "Expected newline"), + LintViolation(1, 58, "Expected newline before annotation"), + LintViolation(1, 70, "Expected newline before annotation"), + LintViolation(1, 75, "Expected newline after last annotation"), + LintViolation(1, 82, "Expected newline"), ).isFormattedAs(formattedCode) } } @@ -729,9 +797,164 @@ class AnnotationRuleTest { """.trimIndent() annotationRuleAssertThat(code) .hasLintViolations( - LintViolation(4, 17, "Multiple annotations should not be placed on the same line as the annotated construct"), - LintViolation(5, 5, "Annotation with parameter(s) should be placed on a separate line prior to the annotated construct"), - LintViolation(5, 25, "Multiple annotations should not be placed on the same line as the annotated construct"), + LintViolation(4, 16, "Expected newline after last annotation"), + LintViolation(5, 18, "Expected newline before annotation"), + LintViolation(5, 24, "Expected newline after last annotation"), + ).isFormattedAs(formattedCode) + } + + @Nested + inner class `Given a class with a primary constructor` { + @Test + fun `Issue 628 - Given an annotation followed by other modifier before the primary constructor (non ktlint_official code style)`() { + val code = + """ + class Foo @Inject internal constructor() + """.trimIndent() + annotationRuleAssertThat(code).hasNoLintViolations() + } + + @Nested + inner class `Given ktlint_official code style` { + @Test + fun `Issue 628 - Given an annotation followed by other modifier before the primary constructor (ktlint_official code style)`() { + val code = + """ + class Foo @Inject internal constructor() + """.trimIndent() + val formattedCode = + """ + class Foo + @Inject + internal constructor() + """.trimIndent() + annotationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasLintViolations( + LintViolation(1, 10, "Expected newline before annotation"), + LintViolation(1, 18, "Expected newline after last annotation"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given an annotation with parameter`() { + val code = + """ + data class Foo @Bar1 @Bar2("bar") @Bar3 @Bar4 constructor(private val foobar: Int) { + fun foo(): String = "foo" + } + """.trimIndent() + val formattedCode = + """ + data class Foo + @Bar1 + @Bar2("bar") + @Bar3 + @Bar4 + constructor(private val foobar: Int) { + fun foo(): String = "foo" + } + """.trimIndent() + annotationRuleAssertThat(code) + .addAdditionalRuleProvider { IndentationRule() } + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasLintViolations( + LintViolation(1, 15, "Expected newline before annotation"), + LintViolation(1, 21, "Expected newline before annotation"), + LintViolation(1, 34, "Expected newline before annotation"), + LintViolation(1, 46, "Expected newline after last annotation"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given annotations without parameters`() { + val code = + """ + data class Foo @Bar1 @Bar2 constructor(private val foobar: Int) { + fun foo(): String = "foo" + } + """.trimIndent() + val formattedCode = + """ + data class Foo + @Bar1 @Bar2 + constructor(private val foobar: Int) { + fun foo(): String = "foo" + } + """.trimIndent() + annotationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasLintViolations( + LintViolation(1, 15, "Expected newline before annotation"), + LintViolation(1, 27, "Expected newline after last annotation"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given single annotation without parameter`() { + val code = + """ + data class Foo @Bar1 constructor(private val foobar: Int) { + fun foo(): String = "foo" + } + """.trimIndent() + val formattedCode = + """ + data class Foo + @Bar1 + constructor(private val foobar: Int) { + fun foo(): String = "foo" + } + """.trimIndent() + annotationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasLintViolations( + LintViolation(1, 15, "Expected newline before annotation"), + LintViolation(1, 21, "Expected newline after last annotation"), + ).isFormattedAs(formattedCode) + } + } + } + + @Test + fun `Given an annotation with parameter preceded by a blank line then do not remove the blank line`() { + val code = + """ + val foo = "foo" + + @Bar("bar") + fun bar() = "bar" + """.trimIndent() + annotationRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given a function literal containing an annotated expression`() { + val code = + """ + val foo = { + @Bar("bar") + foobar { "foobar" } + } + val foo = { @Bar("bar") foobar { "foobar" } } + """.trimIndent() + val formattedCode = + """ + val foo = { + @Bar("bar") + foobar { "foobar" } + } + val foo = { + @Bar("bar") + foobar { "foobar" } + } + """.trimIndent() + annotationRuleAssertThat(code) + .addAdditionalRuleProvider { IndentationRule() } + .hasLintViolations( + LintViolation(5, 12, "Expected newline before annotation"), + LintViolation(5, 24, "Expected newline after last annotation"), + LintViolation(5, 44, "Expected newline"), ).isFormattedAs(formattedCode) } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt index 01dbefdfd5..a234d30270 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IndentationRuleTest.kt @@ -4959,6 +4959,89 @@ internal class IndentationRuleTest { indentationRuleAssertThat(code).hasNoLintViolations() } + @Test + fun `Issue 1916 - Given a class declaration with an annotation before the constructor`() { + val code = + """ + class Foo + @Bar1 @Bar2 + constructor( + foo1: Foo1, + foo2: Foo2, + ) { + fun foo() = "foo" + } + """.trimIndent() + val formattedCode = + """ + class Foo + @Bar1 @Bar2 + constructor( + foo1: Foo1, + foo2: Foo2, + ) { + fun foo() = "foo" + } + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasLintViolations( + LintViolation(2, 1, "Unexpected indentation (0) (should be 4)"), + LintViolation(3, 1, "Unexpected indentation (0) (should be 4)"), + LintViolation(4, 1, "Unexpected indentation (4) (should be 8)"), + LintViolation(5, 1, "Unexpected indentation (4) (should be 8)"), + LintViolation(6, 1, "Unexpected indentation (0) (should be 4)"), + LintViolation(7, 1, "Unexpected indentation (4) (should be 8)"), + LintViolation(8, 1, "Unexpected indentation (0) (should be 4)"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Issue 1916 - Given a class declaration with an annotation before the constructor nad having a super type list`() { + val code = + """ + class Foo + @Bar1 @Bar2 + constructor( + foo1: Foo1, + foo2: Foo2, + ) : Foobar( + "foobar1", + "foobar2", + ) { + fun foo() = "foo" + } + """.trimIndent() + val formattedCode = + """ + class Foo + @Bar1 @Bar2 + constructor( + foo1: Foo1, + foo2: Foo2, + ) : Foobar( + "foobar1", + "foobar2", + ) { + fun foo() = "foo" + } + """.trimIndent() + indentationRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasLintViolations( + LintViolation(2, 1, "Unexpected indentation (0) (should be 4)"), + LintViolation(3, 1, "Unexpected indentation (0) (should be 4)"), + LintViolation(4, 1, "Unexpected indentation (4) (should be 8)"), + LintViolation(5, 1, "Unexpected indentation (4) (should be 8)"), + LintViolation(6, 1, "Unexpected indentation (0) (should be 4)"), + LintViolation(7, 1, "Unexpected indentation (4) (should be 8)"), + LintViolation(8, 1, "Unexpected indentation (4) (should be 8)"), + LintViolation(9, 1, "Unexpected indentation (0) (should be 4)"), + LintViolation(10, 1, "Unexpected indentation (4) (should be 8)"), + LintViolation(11, 1, "Unexpected indentation (0) (should be 4)"), + ).isFormattedAs(formattedCode) + } + private companion object { val INDENT_STYLE_TAB = INDENT_STYLE_PROPERTY to PropertyType.IndentStyleValue.tab diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeArgumentListSpacingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeArgumentListSpacingRuleTest.kt index a821781666..842b162961 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeArgumentListSpacingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeArgumentListSpacingRuleTest.kt @@ -68,4 +68,18 @@ class TypeArgumentListSpacingRuleTest { """.trimIndent() typeArgumentListSpacingRuleAssertThat(code).hasNoLintViolations() } + + @Test + fun `Given an multiline type argument list then do not report a violation`() { + val code = + """ + class Foo { + val list1: List< + @Serializable(MultiplyingIntSerializer::class) + Int, + > + } + """.trimIndent() + typeArgumentListSpacingRuleAssertThat(code).hasNoLintViolations() + } } diff --git a/ktlint-ruleset-test-tooling/src/main/kotlin/com/pinterest/ruleset/testtooling/DumpASTRule.kt b/ktlint-ruleset-test-tooling/src/main/kotlin/com/pinterest/ruleset/testtooling/DumpASTRule.kt index 9cd48b6cf1..b44fafc86a 100644 --- a/ktlint-ruleset-test-tooling/src/main/kotlin/com/pinterest/ruleset/testtooling/DumpASTRule.kt +++ b/ktlint-ruleset-test-tooling/src/main/kotlin/com/pinterest/ruleset/testtooling/DumpASTRule.kt @@ -13,119 +13,121 @@ import org.jetbrains.kotlin.lexer.KtTokens import java.io.PrintStream import java.util.Locale -public class DumpASTRule @JvmOverloads constructor( - private val out: PrintStream = System.err, - private val color: Boolean = false, -) : Rule( - ruleId = RuleId("$TEST_TOOLING_RULESET_ID:dump-ast"), - about = - About( - maintainer = "Ktlint", - repositoryUrl = "https://github.com/pinterest/ktlint", - issueTrackerUrl = "https://github.com/pinterest/ktlint/issues", - ), -) { - private companion object { - val ELEMENT_TYPE_SET = ElementType::class.members.map { it.name }.toSet() - } - - private var lineNumberColumnLength: Int = 0 - private var lastNode: ASTNode? = null - - override fun beforeVisitChildNodes( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, corrected: Boolean) -> Unit, +public class DumpASTRule + @JvmOverloads + constructor( + private val out: PrintStream = System.err, + private val color: Boolean = false, + ) : Rule( + ruleId = RuleId("$TEST_TOOLING_RULESET_ID:dump-ast"), + about = + About( + maintainer = "Ktlint", + repositoryUrl = "https://github.com/pinterest/ktlint", + issueTrackerUrl = "https://github.com/pinterest/ktlint/issues", + ), ) { - if (node.isRoot()) { - lineNumberColumnLength = node - .lastChildLeafOrSelf() - .lineNumberOrUnknown() - .length - lastNode = node.lastChildLeafOrSelf() + private companion object { + val ELEMENT_TYPE_SET = ElementType::class.members.map { it.name }.toSet() } - var level = -1 - var parent: ASTNode? = node - do { - level++ - parent = parent?.treeParent - } while (parent != null) - out.println( - ( - node + private var lineNumberColumnLength: Int = 0 + private var lastNode: ASTNode? = null + + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, corrected: Boolean) -> Unit, + ) { + if (node.isRoot()) { + lineNumberColumnLength = node + .lastChildLeafOrSelf() .lineNumberOrUnknown() - .let { String.format("%${lineNumberColumnLength}s: ", it).dim() } - ) + - " ".repeat(level).dim() + - colorClassName(node.psi.className) + - " (".dim() + colorClassName(elementTypeClassName(node.elementType)) + ")".dim() + - if (node.getChildren(null).isEmpty()) " \"" + node.text.escape().brighten() + "\"" else "", - ) - if (lastNode == node) { - out.println() + .length + lastNode = node.lastChildLeafOrSelf() + } + var level = -1 + var parent: ASTNode? = node + do { + level++ + parent = parent?.treeParent + } while (parent != null) + out.println( - " ".repeat(lineNumberColumnLength) + - " format: () \"\"".dim(), + ( + node + .lineNumberOrUnknown() + .let { String.format("%${lineNumberColumnLength}s: ", it).dim() } + ) + + " ".repeat(level).dim() + + colorClassName(node.psi.className) + + " (".dim() + colorClassName(elementTypeClassName(node.elementType)) + ")".dim() + + if (node.getChildren(null).isEmpty()) " \"" + node.text.escape().brighten() + "\"" else "", ) - if (node.lineNumberOrUnknown() == "Unknown") { + if (lastNode == node) { + out.println() out.println( " ".repeat(lineNumberColumnLength) + - " line_number 'Unknown' is caused by mutations in the AST during formatting".dim(), + " format: () \"\"".dim(), ) + if (node.lineNumberOrUnknown() == "Unknown") { + out.println( + " ".repeat(lineNumberColumnLength) + + " line_number 'Unknown' is caused by mutations in the AST during formatting".dim(), + ) + } + out.println( + " ".repeat(lineNumberColumnLength) + + " legend: ~ = org.jetbrains.kotlin, c.i.p = com.intellij.psi".dim(), + ) + out.println() } - out.println( - " ".repeat(lineNumberColumnLength) + - " legend: ~ = org.jetbrains.kotlin, c.i.p = com.intellij.psi".dim(), - ) - out.println() } - } - - private fun ASTNode.lineNumberOrUnknown(): String { - val lineNumber = - try { - psi - .containingFile - ?.viewProvider - ?.document - ?.getLineNumber(this.startOffset) - ?.let { it + 1 } - ?.toString() - } catch (e: IndexOutOfBoundsException) { - // Due to autocorrect mutations in the AST it can happen that the node's offset becomes invalid. As a result - // the line number can not be determined. - null - } - return lineNumber ?: "Unknown" - } - private fun elementTypeClassName(elementType: IElementType): String { - var name = elementType.toString().substringAfterLast(".").uppercase(Locale.getDefault()) - if (name == "FLOAT_CONSTANT" && elementType == KtTokens.FLOAT_LITERAL) { - // resolve KtNodeTypes.FLOAT_CONSTANT vs KtTokens.FLOAT_LITERAL(FLOAT_CONSTANT) conflict - name = "FLOAT_LITERAL" + private fun ASTNode.lineNumberOrUnknown(): String { + val lineNumber = + try { + psi + .containingFile + ?.viewProvider + ?.document + ?.getLineNumber(this.startOffset) + ?.let { it + 1 } + ?.toString() + } catch (e: IndexOutOfBoundsException) { + // Due to autocorrect mutations in the AST it can happen that the node's offset becomes invalid. As a result + // the line number can not be determined. + null + } + return lineNumber ?: "Unknown" } - if (KtTokens.KEYWORDS.contains(elementType) || KtTokens.SOFT_KEYWORDS.contains(elementType)) { - name = "${name}_KEYWORD" + + private fun elementTypeClassName(elementType: IElementType): String { + var name = elementType.toString().substringAfterLast(".").uppercase(Locale.getDefault()) + if (name == "FLOAT_CONSTANT" && elementType == KtTokens.FLOAT_LITERAL) { + // resolve KtNodeTypes.FLOAT_CONSTANT vs KtTokens.FLOAT_LITERAL(FLOAT_CONSTANT) conflict + name = "FLOAT_LITERAL" + } + if (KtTokens.KEYWORDS.contains(elementType) || KtTokens.SOFT_KEYWORDS.contains(elementType)) { + name = "${name}_KEYWORD" + } + return if (ELEMENT_TYPE_SET.contains(name)) name else elementType.className + "." + elementType } - return if (ELEMENT_TYPE_SET.contains(name)) name else elementType.className + "." + elementType - } - private fun colorClassName(className: String): String { - val name = className.substringAfterLast(".") - return className.substring(0, className.length - name.length).dim() + name - } + private fun colorClassName(className: String): String { + val name = className.substringAfterLast(".") + return className.substring(0, className.length - name.length).dim() + name + } - private fun String.brighten() = optColor(Color.YELLOW) - private fun String.dim() = optColor(Color.DARK_GRAY) - private fun String.optColor(foreground: Color) = if (color) this.color(foreground) else this + private fun String.brighten() = optColor(Color.YELLOW) + private fun String.dim() = optColor(Color.DARK_GRAY) + private fun String.optColor(foreground: Color) = if (color) this.color(foreground) else this - private val Any.className - get() = - this.javaClass.name - .replace("org.jetbrains.kotlin.", "~.") - .replace("com.intellij.psi.", "c.i.p.") + private val Any.className + get() = + this.javaClass.name + .replace("org.jetbrains.kotlin.", "~.") + .replace("com.intellij.psi.", "c.i.p.") - private fun String.escape() = this.replace("\\", "\\\\").replace("\n", "\\n").replace("\t", "\\t").replace("\r", "\\r") -} + private fun String.escape() = this.replace("\\", "\\\\").replace("\n", "\\n").replace("\t", "\\t").replace("\r", "\\r") + }