diff --git a/documentation/snapshot/docs/rules/experimental.md b/documentation/snapshot/docs/rules/experimental.md index 7359e7eafb..a03fb6aae2 100644 --- a/documentation/snapshot/docs/rules/experimental.md +++ b/documentation/snapshot/docs/rules/experimental.md @@ -8,6 +8,36 @@ ktlint_experimental=enabled ``` Also see [enable/disable specific rules](../configuration-ktlint/#disabled-rules). +### Backing property naming + +Allows property names to start with `_` in case the property is a backing property. `ktlint_official` and `android_studio` code styles require the correlated property/function to be defined as `public`. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + class Bar { + // Backing property + private val _elementList = mutableListOf() + val elementList: List + get() = _elementList + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + class Bar { + // Incomplete backing property as public property 'elementList1' is missing + private val _elementList1 = mutableListOf() + + // Invalid backing property as '_elementList2' is not a private property + val _elementList2 = mutableListOf() + val elementList2: List + get() = _elementList2 + } + ``` + +Rule id: `backing-property-naming` (`standard` rule set) + ## Binary expression wrapping Wraps binary expression at the operator reference whenever the binary expression does not fit on the line. In case the binary expression is nested, the expression is evaluated from outside to inside. If the left and right hand sides of the binary expression, after wrapping, fit on a single line then the inner binary expressions will not be wrapped. If one or both inner binary expression still do not fit on a single after wrapping of the outer binary expression, then each of those inner binary expressions will be wrapped. diff --git a/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api b/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api index aa6a95aa80..90d7eb6151 100644 --- a/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api +++ b/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api @@ -43,6 +43,16 @@ public final class com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrapp public static final fun getARGUMENT_LIST_WRAPPING_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId; } +public final class com/pinterest/ktlint/ruleset/standard/rules/BackingPropertyNamingRule : com/pinterest/ktlint/ruleset/standard/StandardRule { + public fun ()V + public fun beforeFirstNode (Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfig;)V + public fun beforeVisitChildNodes (Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;ZLkotlin/jvm/functions/Function3;)V +} + +public final class com/pinterest/ktlint/ruleset/standard/rules/BackingPropertyNamingRuleKt { + public static final fun getBACKING_PROPERTY_NAMING_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId; +} + public final class com/pinterest/ktlint/ruleset/standard/rules/BinaryExpressionWrappingRule : com/pinterest/ktlint/ruleset/standard/StandardRule, com/pinterest/ktlint/rule/engine/core/api/Rule$Experimental { public fun ()V public fun beforeFirstNode (Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfig;)V diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt index 05ddf82b98..25fd4e52da 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt @@ -6,6 +6,7 @@ import com.pinterest.ktlint.rule.engine.core.api.RuleSetId import com.pinterest.ktlint.ruleset.standard.rules.AnnotationRule import com.pinterest.ktlint.ruleset.standard.rules.AnnotationSpacingRule import com.pinterest.ktlint.ruleset.standard.rules.ArgumentListWrappingRule +import com.pinterest.ktlint.ruleset.standard.rules.BackingPropertyNamingRule import com.pinterest.ktlint.ruleset.standard.rules.BinaryExpressionWrappingRule import com.pinterest.ktlint.ruleset.standard.rules.BlankLineBeforeDeclarationRule import com.pinterest.ktlint.ruleset.standard.rules.BlockCommentInitialStarAlignmentRule @@ -104,6 +105,7 @@ public class StandardRuleSetProvider : RuleSetProviderV3(RuleSetId.STANDARD) { RuleProvider { AnnotationRule() }, RuleProvider { AnnotationSpacingRule() }, RuleProvider { ArgumentListWrappingRule() }, + RuleProvider { BackingPropertyNamingRule() }, RuleProvider { BinaryExpressionWrappingRule() }, RuleProvider { BlankLineBeforeDeclarationRule() }, RuleProvider { BlockCommentInitialStarAlignmentRule() }, diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BackingPropertyNamingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BackingPropertyNamingRule.kt new file mode 100644 index 0000000000..3b8e727a49 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BackingPropertyNamingRule.kt @@ -0,0 +1,133 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUN +import com.pinterest.ktlint.rule.engine.core.api.ElementType.IDENTIFIER +import com.pinterest.ktlint.rule.engine.core.api.ElementType.INTERNAL_KEYWORD +import com.pinterest.ktlint.rule.engine.core.api.ElementType.PRIVATE_KEYWORD +import com.pinterest.ktlint.rule.engine.core.api.ElementType.PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.ElementType.PROTECTED_KEYWORD +import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER +import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER_LIST +import com.pinterest.ktlint.rule.engine.core.api.RuleId +import com.pinterest.ktlint.rule.engine.core.api.SinceKtlint +import com.pinterest.ktlint.rule.engine.core.api.SinceKtlint.Status.EXPERIMENTAL +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.android_studio +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig +import com.pinterest.ktlint.rule.engine.core.api.hasModifier +import com.pinterest.ktlint.ruleset.standard.StandardRule +import com.pinterest.ktlint.ruleset.standard.rules.internal.regExIgnoringDiacriticsAndStrokesOnLetters +import org.jetbrains.kotlin.com.intellij.lang.ASTNode + +/** + * https://kotlinlang.org/docs/coding-conventions.html#property-names + * https://developer.android.com/kotlin/style-guide#backing_properties + */ +@SinceKtlint("1.2.0", EXPERIMENTAL) +public class BackingPropertyNamingRule : + StandardRule( + id = "backing-property-naming", + usesEditorConfigProperties = setOf(CODE_STYLE_PROPERTY), + ) { + private var codeStyle = CODE_STYLE_PROPERTY.defaultValue + + override fun beforeFirstNode(editorConfig: EditorConfig) { + codeStyle = editorConfig[CODE_STYLE_PROPERTY] + } + + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + node + .takeIf { node.elementType == PROPERTY } + ?.let { property -> visitProperty(property, emit) } + } + + private fun visitProperty( + property: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + property + .findChildByType(IDENTIFIER) + ?.takeIf { it.text.startsWith("_") } + ?.let { identifier -> + visitBackingProperty(identifier, emit) + } + } + + private fun visitBackingProperty( + identifier: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + identifier + .text + .takeUnless { it.matches(BACKING_PROPERTY_LOWER_CAMEL_CASE_REGEXP) } + ?.let { + emit(identifier.startOffset, "Backing property should start with underscore followed by lower camel case", false) + } + + if (!identifier.treeParent.hasModifier(PRIVATE_KEYWORD)) { + emit(identifier.startOffset, "Backing property not allowed when 'private' modifier is missing", false) + } + + // A backing property can only exist when a correlated public property or function exists + val correlatedPropertyOrFunction = identifier.findCorrelatedPropertyOrFunction() + if (correlatedPropertyOrFunction == null) { + emit(identifier.startOffset, "Backing property is only allowed when a matching property or function exists", false) + } else { + if (codeStyle == android_studio || correlatedPropertyOrFunction.isPublic()) { + return + } else { + emit(identifier.startOffset, "Backing property is only allowed when the matching property or function is public", false) + } + } + } + + private fun ASTNode.findCorrelatedPropertyOrFunction() = findCorrelatedProperty() ?: findCorrelatedFunction() + + private fun ASTNode.findCorrelatedProperty() = + treeParent + ?.treeParent + ?.children() + ?.filter { it.elementType == PROPERTY } + ?.mapNotNull { it.findChildByType(IDENTIFIER) } + ?.firstOrNull { it.text == text.removePrefix("_") } + ?.treeParent + + private fun ASTNode.findCorrelatedFunction(): ASTNode? { + val correlatedFunctionName = "get${capitalizeFirstChar()}" + return treeParent + ?.treeParent + ?.children() + ?.filter { it.elementType == FUN } + ?.filter { it.hasNonEmptyParameterList() } + ?.mapNotNull { it.findChildByType(IDENTIFIER) } + ?.firstOrNull { it.text == correlatedFunctionName } + ?.treeParent + } + + private fun ASTNode.hasNonEmptyParameterList() = + findChildByType(VALUE_PARAMETER_LIST) + ?.children() + ?.none { it.elementType == VALUE_PARAMETER } + ?: false + + private fun ASTNode.capitalizeFirstChar() = + text + .removePrefix("_") + .replaceFirstChar { it.uppercase() } + + private fun ASTNode.isPublic() = + !hasModifier(PRIVATE_KEYWORD) && + !hasModifier(PROTECTED_KEYWORD) && + !hasModifier(INTERNAL_KEYWORD) + + private companion object { + val BACKING_PROPERTY_LOWER_CAMEL_CASE_REGEXP = "_[a-z][a-zA-Z0-9]*".regExIgnoringDiacriticsAndStrokesOnLetters() + } +} + +public val BACKING_PROPERTY_NAMING_RULE_ID: RuleId = BackingPropertyNamingRule().ruleId diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule.kt index 66f361aa65..23fdbc8aa8 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule.kt @@ -3,18 +3,12 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.ElementType.CLASS_BODY import com.pinterest.ktlint.rule.engine.core.api.ElementType.CONST_KEYWORD import com.pinterest.ktlint.rule.engine.core.api.ElementType.FILE -import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUN import com.pinterest.ktlint.rule.engine.core.api.ElementType.GET_KEYWORD import com.pinterest.ktlint.rule.engine.core.api.ElementType.IDENTIFIER -import com.pinterest.ktlint.rule.engine.core.api.ElementType.INTERNAL_KEYWORD import com.pinterest.ktlint.rule.engine.core.api.ElementType.OBJECT_DECLARATION import com.pinterest.ktlint.rule.engine.core.api.ElementType.OVERRIDE_KEYWORD -import com.pinterest.ktlint.rule.engine.core.api.ElementType.PRIVATE_KEYWORD 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.PROTECTED_KEYWORD -import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER -import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER_LIST import com.pinterest.ktlint.rule.engine.core.api.ElementType.VAL_KEYWORD import com.pinterest.ktlint.rule.engine.core.api.RuleId import com.pinterest.ktlint.rule.engine.core.api.SinceKtlint @@ -59,9 +53,6 @@ public class PropertyNamingRule : StandardRule("property-naming") { property.hasCustomGetter() || property.isTopLevelValue() || property.isObjectValue() -> { // Can not reliably determine whether the value is immutable or not } - identifier.text.startsWith("_") -> { - visitBackingProperty(identifier, emit) - } else -> { visitNonConstProperty(identifier, emit) } @@ -69,66 +60,6 @@ public class PropertyNamingRule : StandardRule("property-naming") { } } - private fun visitBackingProperty( - identifier: ASTNode, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - ) { - identifier - .text - .takeUnless { it.matches(BACKING_PROPERTY_LOWER_CAMEL_CASE_REGEXP) } - ?.let { - emit(identifier.startOffset, "Backing property name should start with underscore followed by lower camel case", false) - } - - if (!identifier.treeParent.hasModifier(PRIVATE_KEYWORD)) { - emit(identifier.startOffset, "Backing property name not allowed when 'private' modifier is missing", false) - } - - // A backing property can only exist when a correlated public property or function exists - identifier - .treeParent - ?.treeParent - ?.children() - ?.filter { it.elementType == PROPERTY } - ?.mapNotNull { it.findChildByType(IDENTIFIER) } - ?.firstOrNull { it.text == identifier.text.removePrefix("_") } - ?.treeParent - ?.let { correlatedProperty -> - if (correlatedProperty.isNotPublic()) { - return - } - } - - val correlatedFunctionName = "get${identifier.capitalizeFirstChar()}" - identifier - .treeParent - ?.treeParent - ?.children() - ?.filter { it.elementType == FUN } - ?.filter { it.hasNonEmptyParameterList() } - ?.mapNotNull { it.findChildByType(IDENTIFIER) } - ?.firstOrNull { it.text == correlatedFunctionName } - ?.treeParent - ?.let { correlatedFunction -> - if (correlatedFunction.isNotPublic()) { - return - } - } - - emit(identifier.startOffset, "Backing property name is only allowed when a matching public property or function exists", false) - } - - private fun ASTNode.hasNonEmptyParameterList() = - findChildByType(VALUE_PARAMETER_LIST) - ?.children() - ?.none { it.elementType == VALUE_PARAMETER } - ?: false - - private fun ASTNode.capitalizeFirstChar() = - text - .removePrefix("_") - .replaceFirstChar { it.uppercase() } - private fun visitConstProperty( identifier: ASTNode, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, @@ -158,7 +89,10 @@ public class PropertyNamingRule : StandardRule("property-naming") { identifier .text .takeUnless { it.matches(LOWER_CAMEL_CASE_REGEXP) } - ?.let { + ?.takeUnless { + // Ignore backing properties + it.startsWith("_") + }?.let { emit(identifier.startOffset, "Property name should start with a lowercase letter and use camel case", false) } } @@ -177,11 +111,6 @@ public class PropertyNamingRule : StandardRule("property-naming") { containsValKeyword() && !hasModifier(OVERRIDE_KEYWORD) - private fun ASTNode.isNotPublic() = - !hasModifier(PRIVATE_KEYWORD) && - !hasModifier(PROTECTED_KEYWORD) && - !hasModifier(INTERNAL_KEYWORD) - private fun ASTNode.isTokenKeywordBetweenBackticks() = this .takeIf { it.elementType == IDENTIFIER } @@ -193,7 +122,6 @@ public class PropertyNamingRule : StandardRule("property-naming") { private companion object { val LOWER_CAMEL_CASE_REGEXP = "[a-z][a-zA-Z0-9]*".regExIgnoringDiacriticsAndStrokesOnLetters() val SCREAMING_SNAKE_CASE_REGEXP = "[A-Z][_A-Z0-9]*".regExIgnoringDiacriticsAndStrokesOnLetters() - val BACKING_PROPERTY_LOWER_CAMEL_CASE_REGEXP = "_[a-z][a-zA-Z0-9]*".regExIgnoringDiacriticsAndStrokesOnLetters() const val SERIAL_VERSION_UID_PROPERTY_NAME = "serialVersionUID" val KEYWORDS = setOf(KtTokens.KEYWORDS, KtTokens.SOFT_KEYWORDS) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BackingPropertyRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BackingPropertyRuleTest.kt new file mode 100644 index 0000000000..4111ffc408 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BackingPropertyRuleTest.kt @@ -0,0 +1,347 @@ +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.android_studio +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue.intellij_idea +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.KtlintDocumentationTest +import com.pinterest.ktlint.test.LintViolation +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.junit.jupiter.params.provider.ValueSource + +class BackingPropertyRuleTest { + private val backingPropertyNamingRuleAssertThat = assertThatRule { BackingPropertyNamingRule() } + + @Nested + inner class `Given a backing property correlating with a property` { + @ParameterizedTest(name = "Correlated property name: {0}") + @ValueSource( + strings = [ + "foo", + "føø", + ], + ) + fun `Given a valid property name then do not emit`(propertyName: String) { + val code = + """ + class Foo { + private var _$propertyName = "some-value" + + val $propertyName: String + get() = _$propertyName + } + """.trimIndent() + backingPropertyNamingRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given that the correlated property is implicitly public then do not emit`() { + val code = + """ + class Foo { + private val _elementList = mutableListOf() + + val elementList: List + get() = _elementList + } + """.trimIndent() + backingPropertyNamingRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given that the correlated property is explicitly public then do not emit`() { + val code = + """ + class Foo { + private val _elementList = mutableListOf() + + public val elementList: List + get() = _elementList + } + """.trimIndent() + backingPropertyNamingRuleAssertThat(code).hasNoLintViolations() + } + + @ParameterizedTest(name = "Modifier: {0}") + @ValueSource( + strings = [ + "private", + "protected", + "internal", + ], + ) + fun `Given ktlint_official code style, and the correlated property is non-public then emit`(modifier: String) { + val code = + """ + class Foo { + private val _elementList = mutableListOf() + + $modifier val elementList: List + get() = _elementList + } + """.trimIndent() + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + backingPropertyNamingRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasLintViolationWithoutAutoCorrect(2, 17, "Backing property is only allowed when the matching property or function is public") + } + + @ParameterizedTest(name = "Modifier: {0}") + @ValueSource( + strings = [ + "private", + "protected", + "internal", + ], + ) + fun `Given intellij_idea code style, and the correlated property is non-public then emit`(modifier: String) { + val code = + """ + class Foo { + private val _elementList = mutableListOf() + + $modifier val elementList: List + get() = _elementList + } + """.trimIndent() + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + backingPropertyNamingRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to intellij_idea) + .hasLintViolationWithoutAutoCorrect(2, 17, "Backing property is only allowed when the matching property or function is public") + } + + @ParameterizedTest(name = "Modifier: {0}") + @ValueSource( + strings = [ + "private", + "protected", + "internal", + ], + ) + fun `Given android_studio code style, and the correlated property is non-public then do not emit`(modifier: String) { + val code = + """ + class Foo { + private val _elementList = mutableListOf() + + $modifier val elementList: List + get() = _elementList + } + """.trimIndent() + backingPropertyNamingRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to android_studio) + .hasNoLintViolations() + } + } + + @Nested + inner class `Given a backing property correlating with a function` { + @ParameterizedTest(name = "Correlated property name: {0}") + @CsvSource( + value = [ + "foo,getFoo", + "føø,getFøø", + ], + ) + fun `Given a valid backing property then do not emit`( + propertyName: String, + functionName: String, + ) { + val code = + """ + class Foo { + private var _$propertyName = "some-value" + + fun $functionName(): String = _$propertyName + } + """.trimIndent() + backingPropertyNamingRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given that the correlated function is implicitly public then do not emit`() { + val code = + """ + class Foo { + private val _elementList = mutableListOf() + + fun getElementList(): List = _elementList + } + """.trimIndent() + backingPropertyNamingRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given that the correlated function is explicitly public then do not emit`() { + val code = + """ + class Foo { + private val _elementList = mutableListOf() + + public fun getElementList(): List = _elementList + } + """.trimIndent() + backingPropertyNamingRuleAssertThat(code).hasNoLintViolations() + } + + @ParameterizedTest(name = "Modifier: {0}") + @ValueSource( + strings = [ + "private", + "protected", + "internal", + ], + ) + fun `Given ktlint_official code style, and the correlated function is non-public then emit`(modifier: String) { + val code = + """ + class Foo { + private val _elementList = mutableListOf() + + $modifier fun getElementList(): List = _elementList + } + """.trimIndent() + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + backingPropertyNamingRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to ktlint_official) + .hasLintViolationWithoutAutoCorrect(2, 17, "Backing property is only allowed when the matching property or function is public") + } + + @ParameterizedTest(name = "Modifier: {0}") + @ValueSource( + strings = [ + "private", + "protected", + "internal", + ], + ) + fun `Given intellij_idea code style, and the correlated function is non-public then emit`(modifier: String) { + val code = + """ + class Foo { + private val _elementList = mutableListOf() + + $modifier fun getElementList(): List = _elementList + } + """.trimIndent() + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + backingPropertyNamingRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to intellij_idea) + .hasLintViolationWithoutAutoCorrect(2, 17, "Backing property is only allowed when the matching property or function is public") + } + + @ParameterizedTest(name = "Modifier: {0}") + @ValueSource( + strings = [ + "private", + "protected", + "internal", + ], + ) + fun `Given android_studio code style, and the correlated function is non-public then do not emit`(modifier: String) { + val code = + """ + class Foo { + private val _elementList = mutableListOf() + + $modifier fun getElementList(): List = _elementList + } + """.trimIndent() + backingPropertyNamingRuleAssertThat(code) + .withEditorConfigOverride(CODE_STYLE_PROPERTY to android_studio) + .hasNoLintViolations() + } + + @Test + fun `Given that the correlated function has at least 1 parameter then emit`() { + val code = + """ + class Foo { + private val _elementList = mutableListOf() + + fun getElementList(bar: String): List = _elementList + bar + } + """.trimIndent() + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + backingPropertyNamingRuleAssertThat(code) + .hasLintViolationWithoutAutoCorrect(2, 17, "Backing property is only allowed when a matching property or function exists") + } + } + + @ParameterizedTest(name = "Suppression annotation: {0}") + @ValueSource( + strings = [ + "ktlint:standard:backing-property-naming", + "PropertyName", // IntelliJ IDEA suppression + ], + ) + fun `Given class with a disallowed name which is suppressed`(suppressionName: String) { + val code = + """ + @Suppress("$suppressionName") + val foo = Foo() + """.trimIndent() + backingPropertyNamingRuleAssertThat(code).hasNoLintViolations() + } + + @KtlintDocumentationTest + fun `Ktlint allowed examples`() { + val code = + """ + class Bar { + // Backing property + private val _elementList = mutableListOf() + val elementList: List + get() = _elementList + } + """.trimIndent() + backingPropertyNamingRuleAssertThat(code).hasNoLintViolations() + } + + @KtlintDocumentationTest + fun `Ktlint disallowed examples`() { + val code = + """ + class Bar1 { + // Incomplete backing property as public property 'elementList' or function `getElementList` is missing + private val _elementList = mutableListOf() + } + class Bar2 { + // Invalid backing property as '_elementList' is not a private property + val _elementList = mutableListOf() + val elementList: List + get() = _elementList2 + } + class Bar3 { + // Invalid backing property as 'elementList' is not a public property + // Note: code below is allowed in `android_studio` code style! + private val _elementList = mutableListOf() + internal val elementList: List + get() = _elementList2 + } + """.trimIndent() + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + backingPropertyNamingRuleAssertThat(code) + .hasLintViolations( + LintViolation(3, 17, "Backing property is only allowed when a matching property or function exists", canBeAutoCorrected = false), + LintViolation(7, 9, "Backing property not allowed when 'private' modifier is missing", canBeAutoCorrected = false), + LintViolation(14, 17, "Backing property is only allowed when the matching property or function is public", canBeAutoCorrected = false), + ) + } + + @Test + fun `Given a property name suppressed via 'PropertyName' then also suppress the ktlint violation`() { + val code = + """ + class Foo { + @Suppress("PropertyName") + var FOO = "foo" + } + """.trimIndent() + backingPropertyNamingRuleAssertThat(code).hasNoLintViolations() + } +} diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRuleTest.kt index c084562568..664ee51e28 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRuleTest.kt @@ -11,11 +11,19 @@ import org.junit.jupiter.params.provider.ValueSource class PropertyNamingRuleTest { private val propertyNamingRuleAssertThat = assertThatRule { PropertyNamingRule() } - @Test - fun `Given a valid property name then do not emit`() { + @ParameterizedTest(name = ": {0}") + @ValueSource( + strings = [ + "foo", + "føø", + "_foo", + "_føø", + ], + ) + fun `Given an valid property name then do not emit`(propertyName: String) { val code = """ - var foo = "foo" + var $propertyName = "foo" """.trimIndent() propertyNamingRuleAssertThat(code).hasNoLintViolations() } @@ -118,155 +126,6 @@ class PropertyNamingRuleTest { propertyNamingRuleAssertThat(code).hasNoLintViolations() } - @Nested - inner class `Given a property name starting with '_', and not in screaming case notation` { - @Nested - inner class `Given that a correlated property exists` { - @Test - fun `Given that the correlated property is implicitly public then do not emit`() { - val code = - """ - class Foo { - private val _elementList = mutableListOf() - - val elementList: List - get() = _elementList - } - """.trimIndent() - propertyNamingRuleAssertThat(code).hasNoLintViolations() - } - - @Test - fun `Given that the correlated property is explicitly public then do not emit`() { - val code = - """ - class Foo { - private val _elementList = mutableListOf() - - public val elementList: List - get() = _elementList - } - """.trimIndent() - propertyNamingRuleAssertThat(code).hasNoLintViolations() - } - - @Test - fun `Given that the backing and correlated property contain diacritics then do not emit`() { - val code = - """ - class Foo { - private val _elementŁîšt = mutableListOf() - - val elementŁîšt: List - get() = _elementList - } - """.trimIndent() - propertyNamingRuleAssertThat(code).hasNoLintViolations() - } - - @ParameterizedTest(name = "Modifier: {0}") - @ValueSource( - strings = [ - "private", - "protected", - ], - ) - fun `Given that correlated property is non-public then emit`(modifier: String) { - val code = - """ - class Foo { - private val _elementList = mutableListOf() - - $modifier val elementList: List - get() = _elementList - } - """.trimIndent() - @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") - propertyNamingRuleAssertThat(code) - .hasLintViolationWithoutAutoCorrect(2, 17, "Backing property name is only allowed when a matching public property or function exists") - } - } - - @Nested - inner class `Given that a correlated function exists` { - @Test - fun `Given that the correlated function is implicitly public then do not emit`() { - val code = - """ - class Foo { - private val _elementList = mutableListOf() - - fun getElementList(): List = _elementList - } - """.trimIndent() - propertyNamingRuleAssertThat(code).hasNoLintViolations() - } - - @Test - fun `Given that the correlated function is explicitly public then do not emit`() { - val code = - """ - class Foo { - private val _elementList = mutableListOf() - - public fun getElementList(): List = _elementList - } - """.trimIndent() - propertyNamingRuleAssertThat(code).hasNoLintViolations() - } - - @Test - fun `Given that the backing and correlated function contain diacritics then do not emit`() { - val code = - """ - class Foo { - private val _ëlementŁîšt = mutableListOf() - - fun getËlementŁîšt(): List = _elementList - } - """.trimIndent() - propertyNamingRuleAssertThat(code).hasNoLintViolations() - } - - @ParameterizedTest(name = "Modifier: {0}") - @ValueSource( - strings = [ - "private", - "protected", - "internal", - ], - ) - fun `Given that correlated function is non-public then emit`(modifier: String) { - val code = - """ - class Foo { - private val _elementList = mutableListOf() - - $modifier fun getElementList(): List = _elementList - } - """.trimIndent() - @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") - propertyNamingRuleAssertThat(code) - .hasLintViolationWithoutAutoCorrect(2, 17, "Backing property name is only allowed when a matching public property or function exists") - } - - @Test - fun `Given that the correlated function has at least 1 parameter then emit`() { - val code = - """ - class Foo { - private val _elementList = mutableListOf() - - fun getElementList(bar: String): List = _elementList + bar - } - """.trimIndent() - @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") - propertyNamingRuleAssertThat(code) - .hasLintViolationWithoutAutoCorrect(2, 17, "Backing property name is only allowed when a matching public property or function exists") - } - } - } - @Test fun `Given a local variable then do not emit`() { val code = @@ -285,7 +144,7 @@ class PropertyNamingRuleTest { "PropertyName", // IntelliJ IDEA suppression ], ) - fun `Given class with a disallowed name which is suppressed`(suppressionName: String) { + fun `Given a function with a disallowed name which is suppressed`(suppressionName: String) { val code = """ @Suppress("$suppressionName") @@ -372,11 +231,6 @@ class PropertyNamingRuleTest { var foo2: Foo = Foo() // By definition not immutable - // Backing property - private val _elementList = mutableListOf() - val elementList: List - get() = _elementList - companion object { val foo1 = Foo() // In case developer want to communicate that Foo is mutable val FOO1 = Foo() // In case developer want to communicate that Foo is deeply immutable @@ -396,14 +250,6 @@ class PropertyNamingRuleTest { class Bar { val FOO_BAR = "FOO-BAR" // Class properties always start with lowercase, const is not allowed - - // Incomplete backing property as public property 'elementList1' is missing - private val _elementList1 = mutableListOf() - - // Invalid backing property as '_elementList2' is not a private property - val _elementList2 = mutableListOf() - val elementList2: List - get() = _elementList2 } """.trimIndent() @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") @@ -412,8 +258,6 @@ class PropertyNamingRuleTest { LintViolation(1, 11, "Property name should use the screaming snake case notation when the value can not be changed", canBeAutoCorrected = false), LintViolation(3, 5, "Property name should start with a lowercase letter and use camel case", canBeAutoCorrected = false), LintViolation(6, 9, "Property name should start with a lowercase letter and use camel case", canBeAutoCorrected = false), - LintViolation(9, 17, "Backing property name is only allowed when a matching public property or function exists", canBeAutoCorrected = false), - LintViolation(12, 9, "Backing property name not allowed when 'private' modifier is missing", canBeAutoCorrected = false), ) }