From d1fec0ce29388bd0b731cd431607b401b3f38cd3 Mon Sep 17 00:00:00 2001 From: Stanislav Ovchynnikov Date: Thu, 28 Nov 2024 12:49:16 +0200 Subject: [PATCH] Add missing modifier rule --- .../ComposableModifierMissingRule.kt | 58 +++++++++++++++++++ .../android/ktlintrules/RuleSetProvider.kt | 3 +- .../src/main/java/utils/CustomRulesUtils.kt | 17 ++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 ktlintrules/src/main/java/com/urlaunched/android/ktlintrules/ComposableModifierMissingRule.kt diff --git a/ktlintrules/src/main/java/com/urlaunched/android/ktlintrules/ComposableModifierMissingRule.kt b/ktlintrules/src/main/java/com/urlaunched/android/ktlintrules/ComposableModifierMissingRule.kt new file mode 100644 index 0000000..207e296 --- /dev/null +++ b/ktlintrules/src/main/java/com/urlaunched/android/ktlintrules/ComposableModifierMissingRule.kt @@ -0,0 +1,58 @@ +package com.urlaunched.android.ktlintrules + +import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision +import com.pinterest.ktlint.rule.engine.core.api.ElementType +import com.pinterest.ktlint.rule.engine.core.api.Rule +import com.pinterest.ktlint.rule.engine.core.api.RuleAutocorrectApproveHandler +import com.pinterest.ktlint.rule.engine.core.api.RuleId +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.psi.KtNamedFunction +import utils.CustomRulesUtils.getPackageName +import utils.CustomRulesUtils.isComposable +import utils.CustomRulesUtils.isPreview +import utils.CustomRulesUtils.modifierParameter + +class ComposableModifierMissingRule : + Rule( + RuleId(CUSTOM_RULE_ID), + about = About() + ), + RuleAutocorrectApproveHandler { + override fun beforeVisitChildNodes( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision + ) { + if (node.elementType == ElementType.FUN) { + val function = node.psi as? KtNamedFunction ?: return + if (!function.isComposable || + function.isPreview || + function.receiverTypeReference?.text == MODIFIER_TYPE_REFERENCE || + function.getPackageName()?.contains(DESIGN_RESOURCES_PACKAGE) == true || + modifierMissingExceptions.any { exception -> + function.name?.contains(exception) == true + } + ) { + return + } else { + if (function.modifierParameter() != null) { + return + } else { + emit( + node.startOffset, + MODIFIER_MISSING_ERROR, + false + ) + } + } + } + } + + companion object { + private const val CUSTOM_RULE_ID = "ktlintrules:composable-modifier-missing-rule" + private const val MODIFIER_MISSING_ERROR = + " This @Composable function emits content but doesn't have a modifier parameter." + private const val DESIGN_RESOURCES_PACKAGE = "core.designsystem.resources" + private val modifierMissingExceptions = listOf("Screen", "Route", "SideEffects") + private const val MODIFIER_TYPE_REFERENCE = "Modifier" + } +} \ No newline at end of file diff --git a/ktlintrules/src/main/java/com/urlaunched/android/ktlintrules/RuleSetProvider.kt b/ktlintrules/src/main/java/com/urlaunched/android/ktlintrules/RuleSetProvider.kt index 51a33bd..6f3fa98 100644 --- a/ktlintrules/src/main/java/com/urlaunched/android/ktlintrules/RuleSetProvider.kt +++ b/ktlintrules/src/main/java/com/urlaunched/android/ktlintrules/RuleSetProvider.kt @@ -10,6 +10,7 @@ class RuleSetProvider : RuleSetProviderV3(RuleSetId(CUSTOM_RULE_SET_ID)) { override fun getRuleProviders(): Set = setOf( RuleProvider { ForbiddenImportsRule() }, RuleProvider { ComposableAccessModifiersRule() }, - RuleProvider { DataAccessModifierRule() } + RuleProvider { DataAccessModifierRule() }, + RuleProvider { ComposableModifierMissingRule() } ) } \ No newline at end of file diff --git a/ktlintrules/src/main/java/utils/CustomRulesUtils.kt b/ktlintrules/src/main/java/utils/CustomRulesUtils.kt index 56b064e..84be6af 100644 --- a/ktlintrules/src/main/java/utils/CustomRulesUtils.kt +++ b/ktlintrules/src/main/java/utils/CustomRulesUtils.kt @@ -2,8 +2,11 @@ package utils import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtAnnotationEntry +import org.jetbrains.kotlin.psi.KtCallableDeclaration import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtParameter import org.jetbrains.kotlin.psi.KtPsiUtil import org.jetbrains.kotlin.psi.psiUtil.visibilityModifierType @@ -37,4 +40,18 @@ object CustomRulesUtils { val KtNamedFunction.isComposable: Boolean get() = annotationEntries.any { it.calleeExpression?.text == "Composable" } + + fun KtFunction.modifierParameter(): KtParameter? { + val modifiers = valueParameters.filter { it.isModifier() } + return modifiers.firstOrNull { it.name == "modifier" } ?: modifiers.firstOrNull() + } + + private fun KtCallableDeclaration.isModifier(): Boolean = + typeReference?.text?.contains("Modifier") ?: false + + fun KtFunction.getPackageName(): String? { + val containingFile = this.containingKtFile + val packageDirective = containingFile.packageDirective + return packageDirective?.fqName?.asString() + } } \ No newline at end of file