Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an option to configure ComposePreviewPublic to flag all previews #102

Merged
merged 1 commit into from
Oct 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ interface ComposeKtConfig {

fun ASTNode.config(): ComposeKtConfig = psi.config()

private val PsiElement.hasConfigAttached: Boolean
get() = containingFile.getUserData(Key) != null

fun PsiElement.attach(config: ComposeKtConfig) {
containingFile.putUserData(Key, config)
if (!hasConfigAttached) containingFile.putUserData(Key, config)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ abstract class TwitterDetektRule(

override fun visitClass(klass: KtClass) {
super<Rule>.visitClass(klass)
klass.attach(config)
visitClass(klass, autoCorrect, emitter)
}

override fun visitKtElement(element: KtElement) {
super.visitKtElement(element)
element.attach(config)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found an issue in which visitKtElement was being called while visitFile wasn't 🤷 so I am attaching the config in all entrypoints, and made attach to be fine with that.

when (element.node.elementType) {
KtStubElementTypes.FUNCTION -> {
val function = element.cast<KtFunction>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ internal class KtlintComposeKtConfig(
getValueAsOrPut(key) { getList(key, default.toList()).toSet() } ?: default

override fun getBoolean(key: String, default: Boolean): Boolean =
getValueAsOrPut(key) { properties[ktlintKey(key)]?.getValueAs<String>()?.toBooleanStrictOrNull() } ?: default
getValueAsOrPut(key) { properties[ktlintKey(key)]?.getValueAs<Boolean>() } ?: default
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stores a boolean for realsies now


private fun ktlintKey(key: String): String = "twitter_compose_${key.toSnakeCase()}"
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ package com.twitter.rules.core.ktlint
import com.pinterest.ktlint.core.api.EditorConfigProperties
import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat
import org.ec4j.core.model.Property
import org.ec4j.core.model.PropertyType
import org.ec4j.core.model.PropertyType.LowerCasingPropertyType
import org.ec4j.core.model.PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER
import org.junit.jupiter.api.Test

class KtlintComposeKtConfigTest {
Expand All @@ -15,7 +18,7 @@ class KtlintComposeKtConfigTest {
put("twitter_compose_my_list2", "a , b , c,a".prop)
put("twitter_compose_my_set", "a,b,c,a,b,c".prop)
put("twitter_compose_my_set2", " a, b,c ,a , b , c ".prop)
put("twitter_compose_my_bool", "true".prop)
put("twitter_compose_my_bool", true.prop)
}

private val properties: EditorConfigProperties = mapping
Expand Down Expand Up @@ -70,7 +73,7 @@ class KtlintComposeKtConfigTest {
mapping["my_list2"] = "z,y".prop
mapping["my_set"] = "a".prop
mapping["my_set2"] = "a, b".prop
mapping["my_bool"] = "false".prop
mapping["my_bool"] = false.prop

assertThat(config.getInt("myInt", 0)).isEqualTo(10)
assertThat(config.getString("myString", null)).isEqualTo("abcd")
Expand All @@ -83,4 +86,15 @@ class KtlintComposeKtConfigTest {

private val String.prop: Property
get() = Property.builder().value(this).build()

private val Boolean.prop: Property
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

massive yuck but works

get() = Property.builder()
.type(LowerCasingPropertyType("", "", BOOLEAN_VALUE_PARSER, "true", "false"))
.value(
when (this) {
true -> PropertyType.PropertyValue.valid("true", true)
false -> PropertyType.PropertyValue.valid("false", false)
}
)
.build()
}
2 changes: 2 additions & 0 deletions docs/detekt.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ TwitterCompose:
active: true
PreviewPublic:
active: true
# You can optionally disable that only previews with @PreviewParameter are flagged
# previewPublicOnlyIfParams: false
RememberMissing:
active: true
UnstableCollections:
Expand Down
9 changes: 9 additions & 0 deletions docs/ktlint.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ For `compositionlocal-allowlist` rule you can define a list of `CompositionLocal
twitter_compose_allowed_composition_locals = LocalSomething,LocalSomethingElse
```

### Make it so that all @Preview composables must be not public, no exceptions

In `preview-public-check`, only previews with a `@PreviewParameter` are required to be non-public by default. However, if you want to make it so ALL `@Preview` composables are non-public, you can add this to your `.editorconfig` file:

```editorconfig
[*.{kt,kts}]
twitter_compose_preview_public_only_if_params = false
```

## 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.util.isPreview
Expand All @@ -17,8 +18,12 @@ class ComposePreviewPublic : ComposeKtVisitor {
if (!function.isPreview) return
// We only care about public methods
if (!function.isPublic) return

// If the method is public, none of it's params should be tagged as preview
if (function.valueParameters.none { it.isPreviewParameter }) return
// This is configurable by the `previewPublicOnlyIfParams` config value
if (function.config().getBoolean("previewPublicOnlyIfParams", true)) {
if (function.valueParameters.none { it.isPreviewParameter }) return
}

// If we got here, it's a public method in a @Preview composable with a @PreviewParameter parameter
emitter.report(function, ComposablesPreviewShouldNotBePublic, true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package com.twitter.compose.rules.detekt
import com.twitter.compose.rules.ComposePreviewPublic
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
Expand Down Expand Up @@ -42,6 +43,31 @@ class ComposePreviewPublicCheckTest {
assertThat(errors).isEmpty()
}

@Test
fun `errors when a public preview composable is used when previewPublicOnlyIfParams is false`() {
val config = TestConfig("previewPublicOnlyIfParams" to false)
val ruleWithParams = ComposePreviewPublicCheck(config)

@Language("kotlin")
val code =
"""
@Preview
@Composable
fun MyComposable() { }
@CombinedPreviews
@Composable
fun MyComposable() { }
""".trimIndent()
val errors = ruleWithParams.lint(code)
assertThat(errors).hasSourceLocations(
SourceLocation(3, 5),
SourceLocation(6, 5)
)
for (error in errors) {
assertThat(error).hasMessage(ComposePreviewPublic.ComposablesPreviewShouldNotBePublic)
}
}

@Test
fun `errors when a public preview composable uses preview params`() {
@Language("kotlin")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package com.twitter.rules.core.ktlint

import com.pinterest.ktlint.core.api.UsesEditorConfigProperties
import org.ec4j.core.model.PropertyType
import org.ec4j.core.model.PropertyType.PropertyValueParser

val contentEmittersProperty: UsesEditorConfigProperties.EditorConfigProperty<String> =
UsesEditorConfigProperties.EditorConfigProperty(
Expand Down Expand Up @@ -40,3 +41,16 @@ val compositionLocalAllowlistProperty: UsesEditorConfigProperties.EditorConfigPr
}
}
)

val previewPublicOnlyIfParams: UsesEditorConfigProperties.EditorConfigProperty<Boolean> =
UsesEditorConfigProperties.EditorConfigProperty(
type = PropertyType.LowerCasingPropertyType(
"twitter_compose_preview_public_only_if_params",
"If set to true, it means ",
//
PropertyValueParser.BOOLEAN_VALUE_PARSER,
"true",
"false"
),
defaultValue = true
)
Original file line number Diff line number Diff line change
Expand Up @@ -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.ComposePreviewPublic
import com.twitter.rules.core.ktlint.previewPublicOnlyIfParams
import org.intellij.lang.annotations.Language
import org.junit.jupiter.api.Test

Expand Down Expand Up @@ -66,6 +67,34 @@ class ComposePreviewPublicCheckTest {
)
}

@Test
fun `errors when a public preview composable is used when previewPublicOnlyIfParams is false`() {
@Language("kotlin")
val code =
"""
@Preview
@Composable
fun MyComposable() { }
@CombinedPreviews
@Composable
fun MyComposable() { }
""".trimIndent()
ruleAssertThat(code)
.withEditorConfigOverride(previewPublicOnlyIfParams to false)
.hasLintViolations(
LintViolation(
line = 3,
col = 5,
detail = ComposePreviewPublic.ComposablesPreviewShouldNotBePublic
),
LintViolation(
line = 6,
col = 5,
detail = ComposePreviewPublic.ComposablesPreviewShouldNotBePublic
)
)
}

@Test
fun `passes when a non-public preview composable uses preview params`() {
@Language("kotlin")
Expand Down