Skip to content

Commit

Permalink
Add property .editorconfig property ktlint_enum_entry_name_casing (
Browse files Browse the repository at this point in the history
…#2839)

This property allows enum entry names to be restricted to:
 * `upper_cases`: an enum entry may only contain uppercases, and underscores, and digits, and dicritics on letters and strokes
  * `camel_cases`: an enum entry may only contain CamelCase values, including digits, and dicritics on letters and strokes)
  * `upper_or_camel_case`: allows both the `upper_cases` and `camel_cases` styles as defined in the Kotlin Coding Conventions

Closes #2835
  • Loading branch information
paul-dingemans authored Oct 20, 2024
1 parent 80c4d5d commit 244aa39
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 22 deletions.
4 changes: 4 additions & 0 deletions documentation/snapshot/docs/rules/standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,10 @@ Enum entry names should be uppercase underscore-separated or upper camel-case se
}
```

| Configuration setting | ktlint_official | intellij_idea | android_studio |
|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------:|:-------------:|:--------------:|
| `ktlint_enum_entry_name_casing`</br><i>Choose any of `upper_cases` (an enum entry may only contain uppercases, and underscores, and digits, and dicritics on letters and strokes), `camel_cases` (an enum entry may only contain CamelCase values, including digits, and dicritics on letters and strokes), or `upper_or_camel_case` (allows mixing of uppercase and CamelCase entries as per Kotlin Coding Conventions).</i> | `upper_or_camel_cases` | `upper_or_camel_cases` | `upper_or_camel_cases` |

Rule id: `standard:enum-entry-name-case`

Suppress or disable rule (1)
Expand Down
5 changes: 5 additions & 0 deletions ktlint-rule-engine-core/api/ktlint-rule-engine-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,11 @@ public final class com/pinterest/ktlint/rule/engine/core/api/editorconfig/RuleEx
public static final fun ktLintRuleSetExecutionPropertyName (Lcom/pinterest/ktlint/rule/engine/core/api/RuleSetId;)Ljava/lang/String;
}

public final class com/pinterest/ktlint/rule/engine/core/api/editorconfig/SafeEnumValueParser : org/ec4j/core/model/PropertyType$PropertyValueParser {
public fun <init> (Ljava/lang/Class;)V
public fun parse (Ljava/lang/String;Ljava/lang/String;)Lorg/ec4j/core/model/PropertyType$PropertyValue;
}

public final class com/pinterest/ktlint/rule/engine/core/api/editorconfig/ec4j/EditorConfigPropertyKt {
public static final fun toPropertyBuilderWithValue (Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfigProperty;Ljava/lang/String;)Lorg/ec4j/core/model/Property$Builder;
public static final fun toPropertyBuilderWithValue (Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfigProperty;Lorg/ec4j/core/model/PropertyType$PropertyValue;)Lorg/ec4j/core/model/Property$Builder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,17 @@ import org.ec4j.core.model.PropertyType.PropertyValueParser
import java.util.Locale

/**
* A [PropertyValueParser] implementation that allows only members of a given [Enum] type. This class is almost
* identical to the original [EnumValueParser] provided by ec4j. Difference is that values are trimmed before trying to
* match the enum values.
* A [PropertyValueParser] implementation that allows only members of a given [Enum] type. This class is almost identical to the original
* [EnumValueParser] provided by ec4j. Difference is that values are trimmed before trying to match the enum values.
*
* As the ec4j project has not provided any new release since version 1.0 (2019-08-01) a custom implementation has been
* added.
* As the ec4j project has not provided any new release since version 1.0 (2019-08-01) a custom implementation has been added.
*
* @param <T> the type of the value <T>
*
*/
internal class SafeEnumValueParser<T : Enum<T>>(
enumType: Class<T>,
public class SafeEnumValueParser<T : Enum<T>>(
private val enumType: Class<T>,
) : PropertyValueParser<T> {
private val enumType: Class<T>

init {
this.enumType = enumType
}

override fun parse(
name: String?,
value: String?,
Expand Down
16 changes: 16 additions & 0 deletions ktlint-ruleset-standard/api/ktlint-ruleset-standard.api
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,26 @@ public final class com/pinterest/ktlint/ruleset/standard/rules/DiscouragedCommen
}

public final class com/pinterest/ktlint/ruleset/standard/rules/EnumEntryNameCaseRule : com/pinterest/ktlint/ruleset/standard/StandardRule {
public static final field Companion Lcom/pinterest/ktlint/ruleset/standard/rules/EnumEntryNameCaseRule$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/EnumEntryNameCaseRule$Companion {
public final fun getENUM_ENTRY_NAME_CASING_PROPERTY ()Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfigProperty;
public final fun getENUM_ENTRY_NAME_CASING_PROPERTY_TYPE ()Lorg/ec4j/core/model/PropertyType$LowerCasingPropertyType;
}

public final class com/pinterest/ktlint/ruleset/standard/rules/EnumEntryNameCaseRule$Companion$EnumEntryNameCasing : java/lang/Enum {
public static final field camel_cases Lcom/pinterest/ktlint/ruleset/standard/rules/EnumEntryNameCaseRule$Companion$EnumEntryNameCasing;
public static final field upper_cases Lcom/pinterest/ktlint/ruleset/standard/rules/EnumEntryNameCaseRule$Companion$EnumEntryNameCasing;
public static final field upper_or_camel_cases Lcom/pinterest/ktlint/ruleset/standard/rules/EnumEntryNameCaseRule$Companion$EnumEntryNameCasing;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/pinterest/ktlint/ruleset/standard/rules/EnumEntryNameCaseRule$Companion$EnumEntryNameCasing;
public static fun values ()[Lcom/pinterest/ktlint/ruleset/standard/rules/EnumEntryNameCaseRule$Companion$EnumEntryNameCasing;
}

public final class com/pinterest/ktlint/ruleset/standard/rules/EnumEntryNameCaseRuleKt {
public static final fun getENUM_ENTRY_NAME_CASE_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ 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.SinceKtlint.Status.STABLE
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.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.com.intellij.psi.impl.source.tree.CompositeElement
import org.jetbrains.kotlin.psi.KtEnumEntry
Expand All @@ -16,9 +20,34 @@ import org.jetbrains.kotlin.psi.KtEnumEntry
*/
@SinceKtlint("0.36", EXPERIMENTAL)
@SinceKtlint("0.46", STABLE)
public class EnumEntryNameCaseRule : StandardRule("enum-entry-name-case") {
internal companion object {
val ENUM_ENTRY_IDENTIFIER_REGEX = "[A-Z]([A-Za-z\\d]*|[A-Z_\\d]*)".regExIgnoringDiacriticsAndStrokesOnLetters()
public class EnumEntryNameCaseRule :
StandardRule(
id = "enum-entry-name-case",
usesEditorConfigProperties = setOf(ENUM_ENTRY_NAME_CASING_PROPERTY),
) {
private lateinit var enumEntryCasingRegex: Regex
private lateinit var enumEntryCasingViolation: String
private var x = ENUM_ENTRY_NAME_CASING_PROPERTY.defaultValue

override fun beforeFirstNode(editorConfig: EditorConfig) {
x = editorConfig[ENUM_ENTRY_NAME_CASING_PROPERTY]
when (editorConfig[ENUM_ENTRY_NAME_CASING_PROPERTY]) {
EnumEntryNameCasing.upper_cases -> {
enumEntryCasingRegex = "[A-Z][A-Z_\\d]*".regExIgnoringDiacriticsAndStrokesOnLetters()
enumEntryCasingViolation = "Enum entry name should be uppercase underscore-separated names like \"ENUM_ENTRY\""
}

EnumEntryNameCasing.camel_cases -> {
enumEntryCasingRegex = "[A-Z]([A-Za-z\\d]*)".regExIgnoringDiacriticsAndStrokesOnLetters()
enumEntryCasingViolation = "Enum entry name should be upper camel-case like \"EnumEntry\""
}

EnumEntryNameCasing.upper_or_camel_cases -> {
enumEntryCasingRegex = "[A-Z]([A-Za-z\\d]*|[A-Z_\\d]*)".regExIgnoringDiacriticsAndStrokesOnLetters()
enumEntryCasingViolation =
"Enum entry name should be uppercase underscore-separated names like \"ENUM_ENTRY\" or upper camel-case like \"EnumEntry\""
}
}
}

override fun beforeVisitChildNodes(
Expand All @@ -31,14 +60,47 @@ public class EnumEntryNameCaseRule : StandardRule("enum-entry-name-case") {
val enumEntry = node.psi as? KtEnumEntry ?: return
val name = enumEntry.name ?: return

if (!name.matches(ENUM_ENTRY_IDENTIFIER_REGEX)) {
emit(
node.startOffset,
"Enum entry name should be uppercase underscore-separated names like \"ENUM_ENTRY\" or upper camel-case like \"EnumEntry\"",
false,
)
if (!name.matches(enumEntryCasingRegex)) {
emit(node.startOffset, enumEntryCasingViolation, false)
}
}

public companion object {
@Suppress("EnumEntryName")
public enum class EnumEntryNameCasing {
/**
* Enforce all enum entry names to be uppercase underscore-separated names like "ENUM_ENTRY". Digits, diacritics and strokes are
* allowed.
*/
upper_cases,

/**
* Enforce all enum entry names to be upper camel-case like "EnumEntry". Digits, diacritics and strokes are allowed.
*/
camel_cases,

/**
* Enforce all enum entry names to be uppercase underscore-separated names like "ENUM_ENTRY" or upper camel-case like
* "EnumEntry". Digits, diacritics and strokes are allowed.
*/
upper_or_camel_cases,
}

public val ENUM_ENTRY_NAME_CASING_PROPERTY_TYPE: PropertyType.LowerCasingPropertyType<EnumEntryNameCasing> =
PropertyType.LowerCasingPropertyType(
"ktlint_enum_entry_name_casing",
"Enforce all enum entry names to be uppercase underscore-separated names like \"ENUM_ENTRY\" and/or upper " +
"camel-case like \"EnumEntry\". Digits, diacritics and strokes are always allowed.",
SafeEnumValueParser(EnumEntryNameCasing::class.java),
EnumEntryNameCasing.entries.map { it.name.lowercase() }.toSet(),
)

public val ENUM_ENTRY_NAME_CASING_PROPERTY: EditorConfigProperty<EnumEntryNameCasing> =
EditorConfigProperty(
type = ENUM_ENTRY_NAME_CASING_PROPERTY_TYPE,
defaultValue = EnumEntryNameCasing.upper_or_camel_cases,
)
}
}

public val ENUM_ENTRY_NAME_CASE_RULE_ID: RuleId = EnumEntryNameCaseRule().ruleId
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.pinterest.ktlint.ruleset.standard.rules

import com.pinterest.ktlint.ruleset.standard.rules.EnumEntryNameCaseRule.Companion.ENUM_ENTRY_NAME_CASING_PROPERTY
import com.pinterest.ktlint.ruleset.standard.rules.EnumEntryNameCaseRule.Companion.EnumEntryNameCasing.camel_cases
import com.pinterest.ktlint.ruleset.standard.rules.EnumEntryNameCaseRule.Companion.EnumEntryNameCasing.upper_cases
import com.pinterest.ktlint.ruleset.standard.rules.EnumEntryNameCaseRule.Companion.EnumEntryNameCasing.upper_or_camel_cases
import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule
import com.pinterest.ktlint.test.LintViolation
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test

class EnumEntryNameCaseRuleTest {
Expand Down Expand Up @@ -75,4 +80,43 @@ class EnumEntryNameCaseRuleTest {
""".trimIndent()
enumEntryNameCaseRuleAssertThat(code).hasNoLintViolations()
}

@Nested
inner class `Issue 2835 - Given enum entries in both upper cases and camel cases` {
val code =
"""
enum class SomeEnum {
UPPER_CASE,
CamelCase,
}
""".trimIndent()

@Test
fun `Given that 'ktlint_enum_entry_name_casing' is not set, then allow both upper cases and camel cases`() {
enumEntryNameCaseRuleAssertThat(code).hasNoLintViolations()
}

@Test
fun `Given that 'ktlint_enum_entry_name_casing' is set to 'UPPER_OR_CAMEL_CASES', then allow both upper cases and camel cases`() {
enumEntryNameCaseRuleAssertThat(code)
.withEditorConfigOverride(ENUM_ENTRY_NAME_CASING_PROPERTY to upper_or_camel_cases)
.hasNoLintViolations()
}

@Test
fun `Given that 'ktlint_enum_entry_name_casing' is set to 'UPPER_CASES', then allow only upper cases`() {
@Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length")
enumEntryNameCaseRuleAssertThat(code)
.withEditorConfigOverride(ENUM_ENTRY_NAME_CASING_PROPERTY to upper_cases)
.hasLintViolationWithoutAutoCorrect(3, 5, "Enum entry name should be uppercase underscore-separated names like \"ENUM_ENTRY\"")
}

@Test
fun `Given that 'ktlint_enum_entry_name_casing' is set to 'CAMEL_CASES', then allow only camel cases`() {
@Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length")
enumEntryNameCaseRuleAssertThat(code)
.withEditorConfigOverride(ENUM_ENTRY_NAME_CASING_PROPERTY to camel_cases)
.hasLintViolationWithoutAutoCorrect(2, 5, "Enum entry name should be upper camel-case like \"EnumEntry\"")
}
}
}

0 comments on commit 244aa39

Please sign in to comment.