diff --git a/docs/ktlint.md b/docs/ktlint.md index 9aed6d85..be1b156d 100644 --- a/docs/ktlint.md +++ b/docs/ktlint.md @@ -66,6 +66,15 @@ In `preview-public-check`, only previews with a `@PreviewParameter` are required twitter_compose_preview_public_only_if_params = false ``` +### Allowing matching function names + +The `twitter-compose:naming-check` rule requires all composables that return a value to be lowercased. If you want to allow certain patterns though, you can configure a comma-separated list of matching regexes in your `.editorconfig` file: + +```editorconfig +[*.{kt,kts}] +twitter_compose_allowed_composable_function_names = .*Presenter,.*SomethingElse +``` + ## Disabling a specific rule To disable a rule you have to follow the [instructions from the ktlint documentation](https://github.com/pinterest/ktlint#how-do-i-suppress-an-errors-for-a-lineblockfile), and use the id of the rule you want to disable with the `twitter-compose` tag. diff --git a/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeNaming.kt b/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeNaming.kt index 23f64c74..1b59eaf3 100644 --- a/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeNaming.kt +++ b/rules/common/src/main/kotlin/com/twitter/compose/rules/ComposeNaming.kt @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 package com.twitter.compose.rules +import com.twitter.rules.core.ComposeKtConfig.Companion.config import com.twitter.rules.core.ComposeKtVisitor import com.twitter.rules.core.Emitter import com.twitter.rules.core.report @@ -14,11 +15,18 @@ class ComposeNaming : ComposeKtVisitor { override fun visitComposable(function: KtFunction, autoCorrect: Boolean, emitter: Emitter) { // If it's a block we can't know if there is a return type or not from ktlint if (!function.hasBlockBody()) return - val firstLetter = function.name?.first() ?: return + val functionName = function.name?.takeUnless(String::isEmpty) ?: return + val firstLetter = functionName.first() if (function.returnsValue) { // If it returns value, the composable should start with a lowercase letter if (firstLetter.isUpperCase()) { + // If it's allowed, we don't report it + val isAllowed = function.config().getSet("allowedComposableFunctionNames", emptySet()) + .any { + it.toRegex().matches(functionName) + } + if (isAllowed) return emitter.report(function, ComposablesThatReturnResultsShouldBeLowercase) } } else { diff --git a/rules/detekt/src/test/kotlin/com/twitter/compose/rules/detekt/ComposeNamingCheckTest.kt b/rules/detekt/src/test/kotlin/com/twitter/compose/rules/detekt/ComposeNamingCheckTest.kt index 423dc73e..3a5b43b4 100644 --- a/rules/detekt/src/test/kotlin/com/twitter/compose/rules/detekt/ComposeNamingCheckTest.kt +++ b/rules/detekt/src/test/kotlin/com/twitter/compose/rules/detekt/ComposeNamingCheckTest.kt @@ -3,8 +3,8 @@ package com.twitter.compose.rules.detekt import com.twitter.compose.rules.ComposeNaming -import io.gitlab.arturbosch.detekt.api.Config import io.gitlab.arturbosch.detekt.api.SourceLocation +import io.gitlab.arturbosch.detekt.test.TestConfig import io.gitlab.arturbosch.detekt.test.assertThat import io.gitlab.arturbosch.detekt.test.lint import org.intellij.lang.annotations.Language @@ -12,7 +12,10 @@ import org.junit.jupiter.api.Test class ComposeNamingCheckTest { - private val rule = ComposeNamingCheck(Config.empty) + private val testConfig = TestConfig( + "allowedComposableFunctionNames" to listOf(".*Presenter") + ) + private val rule = ComposeNamingCheck(testConfig) @Test fun `passes when a composable that returns values is lowercase`() { @@ -25,6 +28,17 @@ class ComposeNamingCheckTest { assertThat(rule.lint(code)).isEmpty() } + @Test + fun `passes when a composable that returns values is uppercase but allowed`() { + @Language("kotlin") + val code = + """ + @Composable + fun ProfilePresenter(): Something { } + """.trimIndent() + assertThat(rule.lint(code)).isEmpty() + } + @Test fun `passes when a composable that returns nothing or Unit is uppercase`() { @Language("kotlin") diff --git a/rules/ktlint/src/main/kotlin/com/twitter/compose/rules/ktlint/EditorConfigProperties.kt b/rules/ktlint/src/main/kotlin/com/twitter/compose/rules/ktlint/EditorConfigProperties.kt index 624c6404..f66a911b 100644 --- a/rules/ktlint/src/main/kotlin/com/twitter/compose/rules/ktlint/EditorConfigProperties.kt +++ b/rules/ktlint/src/main/kotlin/com/twitter/compose/rules/ktlint/EditorConfigProperties.kt @@ -54,3 +54,21 @@ val previewPublicOnlyIfParams: UsesEditorConfigProperties.EditorConfigProperty = + UsesEditorConfigProperties.EditorConfigProperty( + type = PropertyType.LowerCasingPropertyType( + "twitter_compose_allowed_composable_function_names", + "A comma separated list of regexes of allowed composable function names", + PropertyType.PropertyValueParser.IDENTITY_VALUE_PARSER, + emptySet() + ), + defaultValue = "", + propertyMapper = { property, _ -> + when { + property?.isUnset == true -> "" + property?.getValueAs() != null -> property.getValueAs() + else -> property?.getValueAs() + } + } + ) diff --git a/rules/ktlint/src/test/kotlin/com/twitter/compose/rules/ktlint/ComposeNamingCheckTest.kt b/rules/ktlint/src/test/kotlin/com/twitter/compose/rules/ktlint/ComposeNamingCheckTest.kt index 1bcc6477..13071a86 100644 --- a/rules/ktlint/src/test/kotlin/com/twitter/compose/rules/ktlint/ComposeNamingCheckTest.kt +++ b/rules/ktlint/src/test/kotlin/com/twitter/compose/rules/ktlint/ComposeNamingCheckTest.kt @@ -5,6 +5,7 @@ package com.twitter.compose.rules.ktlint import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule import com.pinterest.ktlint.test.LintViolation import com.twitter.compose.rules.ComposeNaming +import com.twitter.rules.core.ktlint.allowedComposeNamingNames import org.intellij.lang.annotations.Language import org.junit.jupiter.api.Test @@ -23,6 +24,21 @@ class ComposeNamingCheckTest { namingRuleAssertThat(code).hasNoLintViolations() } + @Test + fun `passes when a composable that returns values is uppercase but allowed`() { + @Language("kotlin") + val code = + """ + @Composable + fun ProfilePresenter(): Something { } + """.trimIndent() + namingRuleAssertThat(code) + .withEditorConfigOverride( + allowedComposeNamingNames to ".*Presenter" + ) + .hasNoLintViolations() + } + @Test fun `passes when a composable that returns nothing or Unit is uppercase`() { @Language("kotlin")