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