Skip to content

Commit

Permalink
Add configuration setting for constant names in property-naming rule (
Browse files Browse the repository at this point in the history
#2893)

Compose requires constant names to follow the PascalCase convention instead of the SCREAMING_SNAKE_CASE convention.

Closes #2637
  • Loading branch information
paul-dingemans authored Dec 3, 2024
1 parent 52ecb3f commit 5cb8c49
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 11 deletions.
7 changes: 7 additions & 0 deletions documentation/snapshot/docs/rules/standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`<br/><i>The naming style ('screaming_snake_case', or 'pascal_case') to be applied on constant properties.</i> | `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)
Expand Down
17 changes: 16 additions & 1 deletion ktlint-ruleset-standard/api/ktlint-ruleset-standard.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -121,18 +135,53 @@ 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 ->
// The keyword sets contain a few 'keywords' which should be ignored. All valid keywords only contain lowercase
// 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<ConstantNamingStyle> =
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<ConstantNamingStyle> =
EditorConfigProperty(
type = CONSTANT_NAMING_PROPERTY_TYPE,
defaultValue = ConstantNamingStyle.screaming_snake_case,
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 5cb8c49

Please sign in to comment.