diff --git a/CHANGELOG.md b/CHANGELOG.md index ab494235eb..1693f9cc53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,9 +30,9 @@ if (node.isRoot()) { ### Added * Wrap blocks in case the max line length is exceeded or in case the block contains a new line `wrapping` ([#1643](https://github.com/pinterest/ktlint/issue/1643)) - * patterns can be read in from `stdin` with the `--patterns-from-stdin` command line options/flags ([#1606](https://github.com/pinterest/ktlint/pull/1606)) * Add basic formatting for context receiver in `indent` rule and new experimental rule `context-receiver-wrapping` ([#1672](https://github.com/pinterest/ktlint/issue/1672)) +* Add naming rules for packages (`package-naming`), classes (`class-naming`), objects (`object-naming`), functions (`function-naming`) and properties (`property-naming`) ([#44](https://github.com/pinterest/ktlint/issue/44)) ### Fixed diff --git a/docs/rules/experimental.md b/docs/rules/experimental.md index 2aa8180829..ee6c8ad464 100644 --- a/docs/rules/experimental.md +++ b/docs/rules/experimental.md @@ -37,6 +37,38 @@ Rewrites the function signature to a single line when possible (e.g. when not ex Rule id: `function-signature` +## Naming + +### Class naming + +Enforce naming of class. + +Rule id: `experimental:class-naming` + +### Function naming + +Enforce naming of function. + +Rule id: `experimental:function-naming` + +### Object naming + +Enforce naming of object. + +Rule id: `experimental:object-naming` + +### Package naming + +Enforce naming of package. + +Rule id: `experimental:package-naming` + +### Property naming + +Enforce naming of property. + +Rule id: `experimental:property-naming` + ## Spacing ### Fun keyword spacing diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ClassNamingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ClassNamingRule.kt new file mode 100644 index 0000000000..6129d8fd6f --- /dev/null +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ClassNamingRule.kt @@ -0,0 +1,29 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.ast.ElementType.CLASS +import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER +import org.jetbrains.kotlin.com.intellij.lang.ASTNode + +/** + * https://kotlinlang.org/docs/coding-conventions.html#naming-rules + */ +public class ClassNamingRule : Rule("$EXPERIMENTAL_RULE_SET_ID:class-naming") { + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + node + .takeIf { node.elementType == CLASS } + ?.findChildByType(IDENTIFIER) + ?.takeUnless { it.text.matches(VALID_CLASS_NAME_REGEXP) } + ?.let { + emit(it.startOffset, "Class name should start with an uppercase letter and use camel case", false) + } + } + + private companion object { + val VALID_CLASS_NAME_REGEXP = Regex("[A-Z][a-zA-Z0-9]*") + } +} diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt index 370a65c712..322b3857b8 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt @@ -42,6 +42,11 @@ public class ExperimentalRuleSetProvider : NullableTypeSpacingRule(), FunctionSignatureRule(), ContextReceiverWrappingRule(), + ClassNamingRule(), + FunctionNamingRule(), + ObjectNamingRule(), + PackageNamingRule(), + PropertyNamingRule(), ) override fun getRuleProviders(): Set = @@ -63,5 +68,10 @@ public class ExperimentalRuleSetProvider : RuleProvider { NullableTypeSpacingRule() }, RuleProvider { FunctionSignatureRule() }, RuleProvider { ContextReceiverWrappingRule() }, + RuleProvider { ClassNamingRule() }, + RuleProvider { FunctionNamingRule() }, + RuleProvider { ObjectNamingRule() }, + RuleProvider { PackageNamingRule() }, + RuleProvider { PropertyNamingRule() }, ) } diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionNamingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionNamingRule.kt new file mode 100644 index 0000000000..eb12ecd041 --- /dev/null +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionNamingRule.kt @@ -0,0 +1,73 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION_ENTRY +import com.pinterest.ktlint.core.ast.ElementType.FUN +import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER +import com.pinterest.ktlint.core.ast.ElementType.MODIFIER_LIST +import com.pinterest.ktlint.core.ast.ElementType.TYPE_REFERENCE +import com.pinterest.ktlint.core.ast.children +import com.pinterest.ktlint.core.ast.nextLeaf +import org.jetbrains.kotlin.com.intellij.lang.ASTNode + +/** + * https://kotlinlang.org/docs/coding-conventions.html#function-names + */ +public class FunctionNamingRule : Rule("$EXPERIMENTAL_RULE_SET_ID:function-naming") { + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + node + .takeIf { node.elementType == FUN } + ?.takeUnless { + node.isFactoryMethod() || + node.isTestMethod() || + node.hasValidFunctionName() + }?.let { + val identifierOffset = + node + .findChildByType(IDENTIFIER) + ?.startOffset + ?: 1 + emit(identifierOffset, "Function name should start with a lowercase letter (except factory methods) and use camel case", false) + } + } + + private fun ASTNode.isFactoryMethod() = + findChildByType(TYPE_REFERENCE)?.text == findChildByType(IDENTIFIER)?.text + + private fun ASTNode.isTestMethod() = + hasBackTickedIdentifier() && hasTestAnnotation() + + private fun ASTNode.hasBackTickedIdentifier() = + findChildByType(IDENTIFIER) + ?.text + .orEmpty() + .matches(BACK_TICKED_FUNCTION_NAME_REGEXP) + + private fun ASTNode.hasTestAnnotation() = + findChildByType(MODIFIER_LIST) + ?.children() + .orEmpty() + .any { it.hasAnnotationWithIdentifierEndingWithTest() } + + private fun ASTNode.hasAnnotationWithIdentifierEndingWithTest() = + elementType == ANNOTATION_ENTRY && + nextLeaf { it.elementType == IDENTIFIER } + ?.text + .orEmpty() + .endsWith("Test") + + private fun ASTNode.hasValidFunctionName() = + findChildByType(IDENTIFIER) + ?.text + .orEmpty() + .matches(VALID_FUNCTION_NAME_REGEXP) + + private companion object { + val VALID_FUNCTION_NAME_REGEXP = Regex("[a-z][a-zA-Z0-9]*") + val BACK_TICKED_FUNCTION_NAME_REGEXP = Regex("`.*`") + } +} diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ObjectNamingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ObjectNamingRule.kt new file mode 100644 index 0000000000..1ce7a1670c --- /dev/null +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ObjectNamingRule.kt @@ -0,0 +1,29 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER +import com.pinterest.ktlint.core.ast.ElementType.OBJECT_DECLARATION +import org.jetbrains.kotlin.com.intellij.lang.ASTNode + +/** + * https://kotlinlang.org/docs/coding-conventions.html#naming-rules + */ +public class ObjectNamingRule : Rule("$EXPERIMENTAL_RULE_SET_ID:object-naming") { + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + node + .takeIf { node.elementType == OBJECT_DECLARATION } + ?.findChildByType(IDENTIFIER) + ?.takeUnless { it.text.matches(VALID_OBJECT_NAME_REGEXP) } + ?.let { + emit(it.startOffset, "Object name should start with an uppercase letter and use camel case", false) + } + } + + private companion object { + val VALID_OBJECT_NAME_REGEXP = Regex("[A-Z][a-zA-Z]*") + } +} diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/PackageNamingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/PackageNamingRule.kt new file mode 100644 index 0000000000..206fc52d57 --- /dev/null +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/PackageNamingRule.kt @@ -0,0 +1,33 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.ast.ElementType.DOT_QUALIFIED_EXPRESSION +import com.pinterest.ktlint.core.ast.ElementType.PACKAGE_DIRECTIVE +import com.pinterest.ktlint.core.ast.ElementType.REFERENCE_EXPRESSION +import com.pinterest.ktlint.core.ast.nextCodeSibling +import org.jetbrains.kotlin.com.intellij.lang.ASTNode + +/** + * https://kotlinlang.org/docs/coding-conventions.html#naming-rules + */ +public class PackageNamingRule : Rule("$EXPERIMENTAL_RULE_SET_ID:package-naming") { + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + node + .takeIf { node.elementType == PACKAGE_DIRECTIVE } + ?.firstChildNode + ?.nextCodeSibling() + ?.takeIf { it.elementType == DOT_QUALIFIED_EXPRESSION || it.elementType == REFERENCE_EXPRESSION } + ?.takeUnless { it.text.matches(VALID_PACKAGE_NAME_REGEXP) } + ?.let { + emit(it.startOffset, "Package name should contain lowercase characters only", false) + } + } + + private companion object { + val VALID_PACKAGE_NAME_REGEXP = Regex("[a-z]+(\\.[a-z]+)*") + } +} diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/PropertyNamingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/PropertyNamingRule.kt new file mode 100644 index 0000000000..4b434e296b --- /dev/null +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/PropertyNamingRule.kt @@ -0,0 +1,143 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.ast.ElementType.CLASS_BODY +import com.pinterest.ktlint.core.ast.ElementType.CONST_KEYWORD +import com.pinterest.ktlint.core.ast.ElementType.FILE +import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER +import com.pinterest.ktlint.core.ast.ElementType.MODIFIER_LIST +import com.pinterest.ktlint.core.ast.ElementType.OBJECT_DECLARATION +import com.pinterest.ktlint.core.ast.ElementType.OVERRIDE_KEYWORD +import com.pinterest.ktlint.core.ast.ElementType.PRIVATE_KEYWORD +import com.pinterest.ktlint.core.ast.ElementType.PROPERTY +import com.pinterest.ktlint.core.ast.ElementType.PROPERTY_ACCESSOR +import com.pinterest.ktlint.core.ast.ElementType.VAL_KEYWORD +import com.pinterest.ktlint.core.ast.children +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType + +/** + * https://kotlinlang.org/docs/coding-conventions.html#function-names + * https://kotlinlang.org/docs/coding-conventions.html#property-names + */ +public class PropertyNamingRule : Rule("$EXPERIMENTAL_RULE_SET_ID:property-naming") { + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + node + .takeIf { node.elementType == PROPERTY } + ?.let { property -> visitProperty(property, emit) } + } + + private fun visitProperty( + property: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + property + .findChildByType(IDENTIFIER) + ?.let { identifier -> + when { + property.hasCustomGetter() -> { + // Can not reliably determine whether the value is immutable or not + } + property.isBackingProperty() -> { + visitBackingProperty(identifier, emit) + } + property.hasConstModifier() || + property.isTopLevelValue() || + property.isObjectValue() -> { + visitConstProperty(identifier, emit) + } + else -> { + visitNonConstProperty(identifier, emit) + } + } + } + } + + private fun visitBackingProperty( + identifier: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + identifier + .text + .takeUnless { it.matches(BACKING_PROPERTY_LOWER_CAMEL_CASE_REGEXP) } + ?.let { + emit(identifier.startOffset, "Backing property name should start with underscore followed by lower camel case", false) + } + } + + private fun visitConstProperty( + identifier: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + identifier + .text + .takeUnless { it.matches(SCREAMING_SNAKE_CASE_REGEXP) } + ?.let { + emit(identifier.startOffset, "Property name should use the screaming snake case notation when the value can not be changed", false) + } + } + + private fun visitNonConstProperty( + identifier: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + identifier + .text + .takeUnless { it.matches(LOWER_CAMEL_CASE_REGEXP) } + ?.let { + emit(identifier.startOffset, "Property name should start with a lowercase letter and use camel case", false) + } + } + + private fun ASTNode.hasCustomGetter() = + findChildByType(PROPERTY_ACCESSOR) + ?.firstChildNode + ?.text == "get" + + private fun ASTNode.hasConstModifier() = + hasModifier(CONST_KEYWORD) + + private fun ASTNode.hasModifier(iElementType: IElementType) = + findChildByType(MODIFIER_LIST) + ?.children() + .orEmpty() + .any { it.elementType == iElementType } + + private fun ASTNode.isTopLevelValue() = + treeParent.elementType == FILE && containsValKeyword() + + private fun ASTNode.containsValKeyword() = + children().any { it.elementType == VAL_KEYWORD } + + private fun ASTNode.isObjectValue() = + treeParent.elementType == CLASS_BODY && + treeParent?.treeParent?.elementType == OBJECT_DECLARATION && + containsValKeyword() && + !hasModifier(OVERRIDE_KEYWORD) + + private fun ASTNode.isBackingProperty() = + takeIf { hasModifier(PRIVATE_KEYWORD) } + ?.findChildByType(IDENTIFIER) + ?.takeIf { it.text.startsWith("_") } + ?.let { identifier -> + this.hasPublicProperty(identifier.text.removePrefix("_")) + } + ?: false + + private fun ASTNode.hasPublicProperty(identifier: String) = + treeParent + .children() + .filter { it.elementType == PROPERTY } + .mapNotNull { it.findChildByType(IDENTIFIER) } + .any { it.text == identifier } + + private companion object { + val LOWER_CAMEL_CASE_REGEXP = Regex("[a-z][a-zA-Z0-9]*") + val SCREAMING_SNAKE_CASE_REGEXP = Regex("[A-Z][_A-Z0-9]*") + val BACKING_PROPERTY_LOWER_CAMEL_CASE_REGEXP = Regex("_[a-z][a-zA-Z0-9]*") + } +} diff --git a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/ClassNamingRuleTest.kt b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/ClassNamingRuleTest.kt new file mode 100644 index 0000000000..9c5542de76 --- /dev/null +++ b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/ClassNamingRuleTest.kt @@ -0,0 +1,36 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.test.KtLintAssertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class ClassNamingRuleTest { + private val classNamingRuleAssertThat = + KtLintAssertThat.assertThatRule { ClassNamingRule() } + + @Test + fun `Given a valid class name then do not emit`() { + val code = + """ + class Foo1 + """.trimIndent() + classNamingRuleAssertThat(code).hasNoLintViolations() + } + + @ParameterizedTest(name = ": {0}") + @ValueSource( + strings = [ + "foo", + "Foo_Bar", + ], + ) + fun `Given an invalid class name then do emit`(className: String) { + val code = + """ + class $className + """.trimIndent() + classNamingRuleAssertThat(code) + .hasLintViolationWithoutAutoCorrect(1, 7, "Class name should start with an uppercase letter and use camel case") + } +} diff --git a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionNamingRuleTest.kt b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionNamingRuleTest.kt new file mode 100644 index 0000000000..1407fafe59 --- /dev/null +++ b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionNamingRuleTest.kt @@ -0,0 +1,73 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.test.KtLintAssertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class FunctionNamingRuleTest { + private val functionNamingRuleAssertThat = + KtLintAssertThat.assertThatRule { FunctionNamingRule() } + + @Test + fun `Given a valid function name then do not emit`() { + val code = + """ + fun foo1() = "foo" + """.trimIndent() + functionNamingRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given a factory method then do not emit`() { + val code = + """ + interface Foo + class FooImpl : Foo + fun Foo(): Foo = FooImpl() + """.trimIndent() + functionNamingRuleAssertThat(code).hasNoLintViolations() + } + + @DisplayName("Given a method with name between backticks") + @Nested + inner class BackTickedFunction { + @Test + fun `Given a function not annotated with a Test annotation then do emit`() { + val code = + """ + fun `Some name`() {} + """.trimIndent() + functionNamingRuleAssertThat(code) + .hasLintViolationWithoutAutoCorrect(1, 5, "Function name should start with a lowercase letter (except factory methods) and use camel case") + } + + @Test + fun `Given a function annotated with a Test annotation then do not emit`() { + val code = + """ + @Test + fun `Some descriptive test name`() {} + """.trimIndent() + functionNamingRuleAssertThat(code).hasNoLintViolations() + } + } + + @ParameterizedTest(name = ": {0}") + @ValueSource( + strings = [ + "Foo", + "Foo_Bar", + ], + ) + fun `Given an invalid function name then do emit`(functionName: String) { + val code = + """ + fun $functionName() = "foo" + """.trimIndent() + functionNamingRuleAssertThat(code) + .hasLintViolationWithoutAutoCorrect(1, 5, "Function name should start with a lowercase letter (except factory methods) and use camel case") + } +} diff --git a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/ObjectNamingRuleTest.kt b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/ObjectNamingRuleTest.kt new file mode 100644 index 0000000000..8837f9f54c --- /dev/null +++ b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/ObjectNamingRuleTest.kt @@ -0,0 +1,37 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.test.KtLintAssertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class ObjectNamingRuleTest { + private val objectNamingRuleAssertThat = + KtLintAssertThat.assertThatRule { ObjectNamingRule() } + + @Test + fun `Given a valid class name then do not emit`() { + val code = + """ + object Foo + """.trimIndent() + objectNamingRuleAssertThat(code).hasNoLintViolations() + } + + @ParameterizedTest(name = ": {0}") + @ValueSource( + strings = [ + "foo", + "Foo1", + "Foo_Bar", + ], + ) + fun `Given an invalid object name then do emit`(className: String) { + val code = + """ + object $className + """.trimIndent() + objectNamingRuleAssertThat(code) + .hasLintViolationWithoutAutoCorrect(1, 8, "Object name should start with an uppercase letter and use camel case") + } +} diff --git a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/PackageNamingRuleTest.kt b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/PackageNamingRuleTest.kt new file mode 100644 index 0000000000..2b7bed6d03 --- /dev/null +++ b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/PackageNamingRuleTest.kt @@ -0,0 +1,68 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.test.KtLintAssertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class PackageNamingRuleTest { + private val packageNamingRuleAssertThat = + KtLintAssertThat.assertThatRule { PackageNamingRule() } + + @Test + fun `Given a valid single level package name then do not emit`() { + val code = + """ + package foo + """.trimIndent() + packageNamingRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given a valid multi level package name then do not emit`() { + val code = + """ + package foo.foo + """.trimIndent() + packageNamingRuleAssertThat(code).hasNoLintViolations() + } + + @ParameterizedTest(name = ": {0}") + @ValueSource( + strings = [ + "Foo", + "foo.Foo", + "foo_bar", + "foo.foo_bar", + "foo1", + "foo.foo1", + ], + ) + fun `Given a package name containing a non-lowercase characters then do emit`(packageName: String) { + val code = + """ + package $packageName + """.trimIndent() + packageNamingRuleAssertThat(code) + .hasLintViolationWithoutAutoCorrect(1, 9, "Package name should contain lowercase characters only") + } + + @Test + fun `Given a package name with containing a non-lowercase characters which is suppressed then do not emit`() { + val code = + """ + @file:Suppress("ktlint:experimental:package-naming") + package foo.fooBar + """.trimIndent() + packageNamingRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given a package name with containing a non-lowercase characters which is suppressed via ktlint directive in comment then do not emit`() { + val code = + """ + package foo.fooBar // ktlint-disable experimental:package-naming + """.trimIndent() + packageNamingRuleAssertThat(code).hasNoLintViolations() + } +} diff --git a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/PropertyNamingRuleTest.kt b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/PropertyNamingRuleTest.kt new file mode 100644 index 0000000000..f1d7538461 --- /dev/null +++ b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/PropertyNamingRuleTest.kt @@ -0,0 +1,128 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.test.KtLintAssertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class PropertyNamingRuleTest { + private val propertyNamingRuleAssertThat = + KtLintAssertThat.assertThatRule { PropertyNamingRule() } + + @Test + fun `Given a valid property name then do not emit`() { + val code = + """ + var foo = "foo" + """.trimIndent() + propertyNamingRuleAssertThat(code).hasNoLintViolations() + } + + @ParameterizedTest(name = ": {0}") + @ValueSource( + strings = [ + "Foo", + "Foo1", + "Foo_Bar", + ], + ) + fun `Given an invalid property name then do emit`(propertyName: String) { + val code = + """ + var $propertyName = "foo" + """.trimIndent() + propertyNamingRuleAssertThat(code) + .hasLintViolationWithoutAutoCorrect(1, 5, "Property name should start with a lowercase letter and use camel case") + } + + @Test + fun `Given a const property name not in screaming case notation then do emit`() { + val code = + """ + const val foo = "foo" + const val FOO_BAR_2 = "foo-bar-2" + """.trimIndent() + propertyNamingRuleAssertThat(code) + .hasLintViolationWithoutAutoCorrect(1, 11, "Property name should use the screaming snake case notation when the value can not be changed") + } + + @Test + fun `Given a top level val property name not in screaming case notation then do emit`() { + val code = + """ + val foo = Foo() + val FOO_BAR = FooBar() + """.trimIndent() + propertyNamingRuleAssertThat(code) + .hasLintViolationWithoutAutoCorrect(1, 5, "Property name should use the screaming snake case notation when the value can not be changed") + } + + @Test + fun `Given an object val property name not having a custom get function and not in screaming case notation then do emit`() { + val code = + """ + class Foo { + companion object { + val foo = Foo() + val FOO_BAR = FooBar() + } + } + """.trimIndent() + propertyNamingRuleAssertThat(code) + .hasLintViolationWithoutAutoCorrect(3, 13, "Property name should use the screaming snake case notation when the value can not be changed") + } + + @Test + fun `Given an object override val property name not having a custom get function and not in screaming case notation then do not emit`() { + val code = + """ + open class Foo { + open val foo = "foo" + } + + val BAR = object : Foo() { + override val foo = "bar" + } + """.trimIndent() + propertyNamingRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given an object val property name having a custom get function and not in screaming case notation then do not emit`() { + val code = + """ + class Foo { + companion object { + val foo + get() = foobar() // Lint can not check whether data is immutable + } + } + """.trimIndent() + propertyNamingRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given a backing val property name having a custom get function and not in screaming case notation then do not emit`() { + val code = + """ + class Foo { + private val _element2List = mutableListOf() + + val element2List: List + get() = _elementList + } + """.trimIndent() + propertyNamingRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given a local variable then do not emit`() { + val code = + """ + fun foo() { + val bar2 = "bar" + } + """.trimIndent() + propertyNamingRuleAssertThat(code).hasNoLintViolations() + } +}