diff --git a/diktat-analysis.yml b/diktat-analysis.yml index ab75eaf7d0..523c1d222b 100644 --- a/diktat-analysis.yml +++ b/diktat-analysis.yml @@ -300,6 +300,9 @@ # Checks that annotation is on a single line - name: ANNOTATION_NEW_LINE enabled: true +# Checks that method annotated with `Preview` annotation is private and has Preview suffix +- name: PREVIEW_ANNOTATION + enabled: true # Checks that enum structure is correct: enum entries should be separated by comma and line break and last entry should have semicolon in the end. - name: ENUMS_SEPARATED enabled: true diff --git a/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/constants/Warnings.kt b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/constants/Warnings.kt index e7ffb0228a..f30154d0d7 100644 --- a/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/constants/Warnings.kt +++ b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/constants/Warnings.kt @@ -131,6 +131,7 @@ enum class Warnings( WRONG_WHITESPACE(true, "3.8.1", "incorrect usage of whitespaces for code separation"), TOO_MANY_CONSECUTIVE_SPACES(true, "3.8.1", "too many consecutive spaces"), ANNOTATION_NEW_LINE(true, "3.12.1", "annotations must be on new line"), + PREVIEW_ANNOTATION(false, "3.12.2", "method, annotated with `@Preview` annotation should be private and has `Preview` suffix"), ENUMS_SEPARATED(true, "3.9.1", "enum is incorrectly formatted"), WHEN_WITHOUT_ELSE(true, "3.11.1", "each 'when' statement must have else at the end"), LONG_NUMERICAL_VALUES_SEPARATED(true, "3.14.2", "long numerical values should be separated with underscore"), diff --git a/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/DiktatRuleSetFactoryImpl.kt b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/DiktatRuleSetFactoryImpl.kt index 61d9db3357..de9c4ae99e 100644 --- a/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/DiktatRuleSetFactoryImpl.kt +++ b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/DiktatRuleSetFactoryImpl.kt @@ -27,6 +27,7 @@ import com.saveourtool.diktat.ruleset.rules.chapter3.LongNumericalValuesSeparate import com.saveourtool.diktat.ruleset.rules.chapter3.MagicNumberRule import com.saveourtool.diktat.ruleset.rules.chapter3.MultipleModifiersSequence import com.saveourtool.diktat.ruleset.rules.chapter3.NullableTypeRule +import com.saveourtool.diktat.ruleset.rules.chapter3.PreviewAnnotationRule import com.saveourtool.diktat.ruleset.rules.chapter3.RangeConventionalRule import com.saveourtool.diktat.ruleset.rules.chapter3.SingleLineStatementsRule import com.saveourtool.diktat.ruleset.rules.chapter3.SortRule @@ -159,6 +160,7 @@ class DiktatRuleSetFactoryImpl : DiktatRuleSetFactory { ::LongNumericalValuesSeparatedRule, ::NestedFunctionBlock, ::AnnotationNewLineRule, + ::PreviewAnnotationRule, ::SortRule, ::EnumsSeparated, ::StringConcatenationRule, diff --git a/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/PreviewAnnotationRule.kt b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/PreviewAnnotationRule.kt new file mode 100644 index 0000000000..ebd648a105 --- /dev/null +++ b/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/PreviewAnnotationRule.kt @@ -0,0 +1,87 @@ +package com.saveourtool.diktat.ruleset.rules.chapter3 + +import com.saveourtool.diktat.common.config.rules.RulesConfig +import com.saveourtool.diktat.ruleset.constants.Warnings.PREVIEW_ANNOTATION +import com.saveourtool.diktat.ruleset.rules.DiktatRule +import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType +import com.saveourtool.diktat.ruleset.utils.getIdentifierName + +import org.jetbrains.kotlin.KtNodeTypes.ANNOTATION_ENTRY +import org.jetbrains.kotlin.KtNodeTypes.FUN +import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.psiUtil.isPrivate + +/** + * This rule checks, whether the method has `@Preview` annotation (Jetpack Compose) + * If so, such method should be private and function name should end with `Preview` suffix + */ +class PreviewAnnotationRule(configRules: List) : DiktatRule( + NAME_ID, + configRules, + listOf(PREVIEW_ANNOTATION) +) { + override fun logic(node: ASTNode) { + if (node.elementType == FUN) { + checkFunctionSignature(node) + } + } + + private fun checkFunctionSignature(node: ASTNode) { + node.findChildByType(MODIFIER_LIST)?.let { modList -> + doCheck(node, modList) + } + } + + private fun doCheck(functionNode: ASTNode, modeList: ASTNode) { + if (modeList.getAllChildrenWithType(ANNOTATION_ENTRY).isEmpty()) { + return + } + + val previewAnnotationNode = modeList.getAllChildrenWithType(ANNOTATION_ENTRY).firstOrNull { + it.text.contains("$ANNOTATION_SYMBOL$PREVIEW_ANNOTATION_TEXT") + } + + previewAnnotationNode?.let { + val functionName = functionNode.getIdentifierName()?.text ?: return + + // warn if function is not private + if (!((functionNode.psi as KtNamedFunction).isPrivate())) { + PREVIEW_ANNOTATION.warnAndFix( + configRules, + emitWarn, + isFixMode, + "$functionName method should be private", + functionNode.startOffset, + functionNode + ) { + // provide fix + } + } + + // warn if function has no `Preview` suffix + if (!isMethodHasPreviewSuffix(functionName)) { + PREVIEW_ANNOTATION.warnAndFix( + configRules, + emitWarn, + isFixMode, + "$functionName method should has `Preview` suffix", + functionNode.startOffset, + functionNode + ) { + // provide fix + } + } + } + } + + private fun isMethodHasPreviewSuffix(functionName: String) = + functionName.contains(PREVIEW_ANNOTATION_TEXT) + + companion object { + const val ANNOTATION_SYMBOL = "@" + const val NAME_ID = "preview-annotation" + const val PREVIEW_ANNOTATION_TEXT = "Preview" + } +} diff --git a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml index 1c481bc3c9..d7f8718fed 100644 --- a/diktat-rules/src/main/resources/diktat-analysis-huawei.yml +++ b/diktat-rules/src/main/resources/diktat-analysis-huawei.yml @@ -299,6 +299,9 @@ # Checks that annotation is on a single line - name: ANNOTATION_NEW_LINE enabled: true +# Checks that method annotated with `Preview` annotation is private and has Preview suffix +- name: PREVIEW_ANNOTATION + enabled: true # Checks that enum structure is correct: enum entries should be separated by comma and line break and last entry should have semicolon in the end. - name: ENUMS_SEPARATED enabled: true diff --git a/diktat-rules/src/main/resources/diktat-analysis.yml b/diktat-rules/src/main/resources/diktat-analysis.yml index 7bd0bd3ab9..2f276637c6 100644 --- a/diktat-rules/src/main/resources/diktat-analysis.yml +++ b/diktat-rules/src/main/resources/diktat-analysis.yml @@ -299,6 +299,9 @@ # Checks that annotation is on a single line - name: ANNOTATION_NEW_LINE enabled: true +# Checks that method annotated with `Preview` annotation is private and has Preview suffix +- name: PREVIEW_ANNOTATION + enabled: true # Checks that enum structure is correct: enum entries should be separated by comma and line break and last entry should have semicolon in the end. - name: ENUMS_SEPARATED enabled: true diff --git a/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/PreviewAnnotationWarnTest.kt b/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/PreviewAnnotationWarnTest.kt new file mode 100644 index 0000000000..817182217b --- /dev/null +++ b/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/PreviewAnnotationWarnTest.kt @@ -0,0 +1,67 @@ +package com.saveourtool.diktat.ruleset.chapter3 + +import com.saveourtool.diktat.api.DiktatError +import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID +import com.saveourtool.diktat.ruleset.constants.Warnings +import com.saveourtool.diktat.ruleset.rules.chapter3.PreviewAnnotationRule +import com.saveourtool.diktat.util.LintTestBase +import generated.WarningNames +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test + + +class PreviewAnnotationWarnTest : LintTestBase(::PreviewAnnotationRule) { + private val ruleId = "$DIKTAT_RULE_SET_ID:${PreviewAnnotationRule.NAME_ID}" + + @Test + @Tag(WarningNames.PREVIEW_ANNOTATION) + fun `no warn`() { + lintMethod( + """ + |@Preview + |@Composable + |private fun BannerPreview() {} + """.trimMargin() + ) + } + + @Test + @Tag(WarningNames.PREVIEW_ANNOTATION) + fun `method is not private`() { + lintMethod( + """ + |@Preview + |@Composable + |fun BannerPreview() {} + """.trimMargin(), + DiktatError(1, 1, ruleId, "${Warnings.PREVIEW_ANNOTATION.warnText()} BannerPreview method should be private", false), + ) + } + + @Test + @Tag(WarningNames.PREVIEW_ANNOTATION) + fun `method has no preview suffix`() { + lintMethod( + """ + |@Preview + |@Composable + |private fun Banner() {} + """.trimMargin(), + DiktatError(1, 1, ruleId, "${Warnings.PREVIEW_ANNOTATION.warnText()} Banner method should has `Preview` suffix", false), + ) + } + + @Test + @Tag(WarningNames.PREVIEW_ANNOTATION) + fun `method has no preview suffix and is not private`() { + lintMethod( + """ + |@Preview + |@Composable + |fun Banner() {} + """.trimMargin(), + DiktatError(1, 1, ruleId, "${Warnings.PREVIEW_ANNOTATION.warnText()} Banner method should be private", false), + DiktatError(1, 1, ruleId, "${Warnings.PREVIEW_ANNOTATION.warnText()} Banner method should has `Preview` suffix", false), + ) + } +} diff --git a/info/available-rules.md b/info/available-rules.md index b6fd059de9..1b2fce484a 100644 --- a/info/available-rules.md +++ b/info/available-rules.md @@ -83,6 +83,7 @@ | 3 | 3.10.2 | LOCAL_VARIABLE_EARLY_DECLARATION | Check: warns if a local variable is declared not immediately before its usage.
Fix (not implemented yet): moves the variable declaration. | no | no | add auto fix | | 3 | 3.11.1 | WHEN_WITHOUT_ELSE | Check: warns if a `when` statement does not have `else` in the end.
Fix: adds `else` when a statement doesn't have it. | yes | no | - | If a `when` statement of the enum or sealed type contains all values of the enum, there is no need to have the "else" branch. | | 3 | 3.12.1 | ANNOTATION_NEW_LINE | Check: warns if an annotation is not on a new single line. | yes | no | - | +| 3 | 3.12.2 | PREVIEW_ANNOTATION | Check: warns if method, annotated with `@Preview` is not private or has no `Preview` suffix. | yes | no | - | | 3 | 3.14.1 | WRONG_MULTIPLE_MODIFIERS_ORDER | Check: warns if the multiple modifiers in the sequence are in the wrong order. Value identifier supported in Kotlin 1.5 | yes | no | - | | 3 | 3.14.2 | LONG_NUMERICAL_VALUES_SEPARATED | Check: warns if the value of the integer or float constant is too big. | no | maxNumberLength maxBlockLength | - | | 3 | 3.14.3 | MAGIC_NUMBER | Check: warns if there are magic numbers in the code. | no | ignoreNumbers, ignoreHashCodeFunction, ignorePropertyDeclaration, ignoreLocalVariableDeclaration, ignoreConstantDeclaration, ignoreCompanionObjectPropertyDeclaration, ignoreEnums, ignoreRanges, ignoreExtensionFunctions | no | diff --git a/info/guide/guide-chapter-3.md b/info/guide/guide-chapter-3.md index 5ee40294ca..8a5ed099e7 100644 --- a/info/guide/guide-chapter-3.md +++ b/info/guide/guide-chapter-3.md @@ -964,7 +964,8 @@ The compiler can issue a warning when it is missing. ### 3.12 Annotations - +This section contains recommendations regarding annotations. +#### 3.12.1 Whitespaces and newlines for annotations Each annotation applied to a class, method or constructor should be placed on its own line. Consider the following examples: 1. Annotations applied to the class, method or constructor are placed on separate lines (one annotation per line). @@ -988,6 +989,16 @@ fun getNameIfPresent() { /* ... */ } ```kotlin @MustBeDocumented @CustomAnnotation val loader: DataLoader ``` +#### 3.12.2 Preview annotation +`@Preview` (Jetpack Compose) functions should end with 'Preview' suffix and are also be private + +**Valid example**: +```kotlin +@Preview +@Composable +private fun BannerPreview() {} +``` + ### 3.13 Block comments