diff --git a/documentation/snapshot/docs/rules/standard.md b/documentation/snapshot/docs/rules/standard.md index fc3872c02b..78130593f0 100644 --- a/documentation/snapshot/docs/rules/standard.md +++ b/documentation/snapshot/docs/rules/standard.md @@ -1505,6 +1505,13 @@ Enforce naming of property. This rule is suppressed whenever the IntelliJ IDEA inspection suppression `PropertyName`, `ConstPropertyName`, `ObjectPropertyName` or `PrivatePropertyName` is used. +| Configuration setting | ktlint_official | intellij_idea | android_studio | +|:-----------------------------------------------------------------------------------------------------------------------------------------------|:----------------------:|:----------------------:|:----------------------:| +| `ktlint_property_naming_constant_naming`
The naming style ('screaming_snake_case', or 'pascal_case') to be applied on constant properties. | `screaming_snake_case` | `screaming_snake_case` | `screaming_snake_case` | + +!!! note + When using Compose, you might want to configure the `ktlint_property_naming_constant_naming-naming` rule with `.editorconfig` property `ktlint_property_naming_constant_naming = pascal_case`. + Rule id: `standard:property-naming` Suppress or disable rule (1) diff --git a/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api b/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api index 7633c64efc..fde175df42 100644 --- a/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api +++ b/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api @@ -716,11 +716,26 @@ public final class com/pinterest/ktlint/ruleset/standard/rules/ParameterWrapping } public final class com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule : com/pinterest/ktlint/ruleset/standard/StandardRule { - public static final field SERIAL_VERSION_UID_PROPERTY_NAME Ljava/lang/String; + public static final field Companion Lcom/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule$Companion; 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;Lkotlin/jvm/functions/Function3;)V } +public final class com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule$Companion { + public final fun getCONSTANT_NAMING_PROPERTY ()Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfigProperty; + public final fun getCONSTANT_NAMING_PROPERTY_TYPE ()Lorg/ec4j/core/model/PropertyType$LowerCasingPropertyType; +} + +public final class com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule$Companion$ConstantNamingStyle : java/lang/Enum { + public static final field pascal_case Lcom/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule$Companion$ConstantNamingStyle; + public static final field screaming_snake_case Lcom/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule$Companion$ConstantNamingStyle; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getRegEx ()Lkotlin/text/Regex; + public static fun valueOf (Ljava/lang/String;)Lcom/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule$Companion$ConstantNamingStyle; + public static fun values ()[Lcom/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule$Companion$ConstantNamingStyle; +} + public final class com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRuleKt { public static final fun getPROPERTY_NAMING_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/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 2139e0b1fb..fbf04822e2 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 @@ -16,9 +16,13 @@ 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.SinceKtlint.Status.STABLE import com.pinterest.ktlint.rule.engine.core.api.children +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.SafeEnumValueParser 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.ec4j.core.model.PropertyType import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens @@ -28,7 +32,17 @@ import org.jetbrains.kotlin.lexer.KtTokens */ @SinceKtlint("0.48", EXPERIMENTAL) @SinceKtlint("1.0", STABLE) -public class PropertyNamingRule : StandardRule("property-naming") { +public class PropertyNamingRule : + StandardRule( + id = "property-naming", + usesEditorConfigProperties = setOf(CONSTANT_NAMING_PROPERTY), + ) { + private var constantNamingProperty = CONSTANT_NAMING_PROPERTY.defaultValue + + override fun beforeFirstNode(editorConfig: EditorConfig) { + constantNamingProperty = editorConfig[CONSTANT_NAMING_PROPERTY] + } + override fun beforeVisitChildNodes( node: ASTNode, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision, @@ -74,7 +88,7 @@ public class PropertyNamingRule : StandardRule("property-naming") { // private const val serialVersionUID: Long = 123 // } it == SERIAL_VERSION_UID_PROPERTY_NAME - }?.takeUnless { it.matches(SCREAMING_SNAKE_CASE_REGEXP) } + }?.takeUnless { it.matches(constantNamingProperty.regEx) } ?.let { emit( identifier.startOffset, @@ -121,11 +135,10 @@ public class PropertyNamingRule : StandardRule("property-naming") { .removeSurrounding("`") .let { KEYWORDS.contains(it) } - 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() - const val SERIAL_VERSION_UID_PROPERTY_NAME = "serialVersionUID" - val KEYWORDS = + public companion object { + private val LOWER_CAMEL_CASE_REGEXP = "[a-z][a-zA-Z0-9]*".regExIgnoringDiacriticsAndStrokesOnLetters() + private const val SERIAL_VERSION_UID_PROPERTY_NAME = "serialVersionUID" + private val KEYWORDS = setOf(KtTokens.KEYWORDS, KtTokens.SOFT_KEYWORDS) .flatMap { tokenSet -> tokenSet.types.mapNotNull { it.debugName } } .filterNot { keyword -> @@ -133,6 +146,42 @@ public class PropertyNamingRule : StandardRule("property-naming") { // characters keyword.any { it.isUpperCase() } }.toSet() + + @Suppress("EnumEntryName") + public enum class ConstantNamingStyle( + public val regEx: Regex, + ) { + /** + * The name of a constant must start with an uppercase character followed by zero or more uppercase characters, numbers, or + * underscore characters to separate words in the name. The latin characters may also be combined with strokes and diacritics. + */ + screaming_snake_case("[A-Z][_A-Z0-9]*".regExIgnoringDiacriticsAndStrokesOnLetters()), + + /** + * The name of a constant must start with an uppercase character followed by zero or more uppercase characters or numbers. Each + * word in the name should start with an uppercase character. The latin characters may also be combined with strokes and + * diacritics. + */ + pascal_case("[A-Z][a-zA-Z0-9]*".regExIgnoringDiacriticsAndStrokesOnLetters()), + } + + public val CONSTANT_NAMING_PROPERTY_TYPE: + PropertyType.LowerCasingPropertyType = + PropertyType.LowerCasingPropertyType( + "ktlint_property_naming_constant_naming", + "The naming style ('screaming_snake_case', or 'pascal_case') to be applied on constant properties. All code styles use " + + "'screaming_snake_case' code as default.", + SafeEnumValueParser(ConstantNamingStyle::class.java), + ConstantNamingStyle.entries + .map { it.name } + .toSet(), + ) + + public val CONSTANT_NAMING_PROPERTY: EditorConfigProperty = + EditorConfigProperty( + type = CONSTANT_NAMING_PROPERTY_TYPE, + defaultValue = ConstantNamingStyle.screaming_snake_case, + ) } } 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 66bff3e1d0..67adbc66e3 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 @@ -1,5 +1,6 @@ package com.pinterest.ktlint.ruleset.standard.rules +import com.pinterest.ktlint.ruleset.standard.rules.PropertyNamingRule.Companion.ConstantNamingStyle.pascal_case import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule import com.pinterest.ktlint.test.KtlintDocumentationTest import com.pinterest.ktlint.test.LintViolation @@ -47,16 +48,47 @@ class PropertyNamingRuleTest { } @Test - fun `Given a const property name not in screaming case notation then do emit`() { + fun `Given the default constant naming style, and a const property name not in screaming case notation then do emit`() { val code = """ const val foo = "foo" + const val FOO = "foo" const val FOO_BAR_2 = "foo-bar-2" const val ŸÈŠ_THÎS_IS_ALLOWED_123 = "Yes this is allowed" + const val Foo = "foo" + const val FooBar2 = "foo-bar-2" + const val ŸèšThîsIsAllowed123 = "Yes this is allowed" """.trimIndent() - @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") propertyNamingRuleAssertThat(code) - .hasLintViolationWithoutAutoCorrect(1, 11, "Property name should use the screaming snake case notation when the value can not be changed") + .hasLintViolationsWithoutAutoCorrect( + LintViolation(1, 11, "Property name should use the screaming snake case notation when the value can not be changed"), + LintViolation(5, 11, "Property name should use the screaming snake case notation when the value can not be changed"), + LintViolation(6, 11, "Property name should use the screaming snake case notation when the value can not be changed"), + LintViolation(7, 11, "Property name should use the screaming snake case notation when the value can not be changed"), + ) + } + + @Test + fun `Given the pascal_case constant naming style, and a const property name not in pascal_case notation then do emit`() { + val code = + """ + const val foo = "foo" + const val FOO = "foo" + const val FOO_BAR_2 = "foo-bar-2" + const val ŸÈŠ_THÎS_IS_ALLOWED_123 = "Yes this is allowed" + const val Foo = "foo" + const val FooBar2 = "foo-bar-2" + const val ŸèšThîsIsAllowed123 = "Yes this is allowed" + """.trimIndent() + propertyNamingRuleAssertThat(code) + .withEditorConfigOverride(PropertyNamingRule.CONSTANT_NAMING_PROPERTY to pascal_case) + .hasLintViolationsWithoutAutoCorrect( + LintViolation(1, 11, "Property name should use the screaming snake case notation when the value can not be changed"), + // FOO cannot be reported as not meeting the pascal case requirement as it could be an abbreviation of 3 separate words + // starting with 'F', 'O' and 'O' respectively + LintViolation(3, 11, "Property name should use the screaming snake case notation when the value can not be changed"), + LintViolation(4, 11, "Property name should use the screaming snake case notation when the value can not be changed"), + ) } @Test