From cbdff6807608d02661eaabd75992e0ff1f5863af Mon Sep 17 00:00:00 2001 From: paul-dingemans Date: Sat, 2 Dec 2023 16:36:19 +0100 Subject: [PATCH 1/2] Allow property, function and class name to be same as keyword wrapped with backticks Closes #2352 --- .../ruleset/standard/rules/ClassNamingRule.kt | 19 +++++++++- .../standard/rules/FunctionNamingRule.kt | 35 +++++++++++++------ .../standard/rules/PropertyNamingRule.kt | 18 ++++++++++ .../standard/rules/ClassNamingRuleTest.kt | 21 +++++++++++ .../standard/rules/FunctionNamingRuleTest.kt | 21 +++++++++++ .../standard/rules/PropertyNamingRuleTest.kt | 25 +++++++++++++ 6 files changed, 128 insertions(+), 11 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ClassNamingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ClassNamingRule.kt index db40767697..78f4b9a518 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ClassNamingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ClassNamingRule.kt @@ -11,6 +11,7 @@ import com.pinterest.ktlint.rule.engine.core.api.SinceKtlint.Status.EXPERIMENTAL import com.pinterest.ktlint.ruleset.standard.StandardRule import com.pinterest.ktlint.ruleset.standard.rules.internal.regExIgnoringDiacriticsAndStrokesOnLetters import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.lexer.KtTokens /** * https://kotlinlang.org/docs/coding-conventions.html#naming-rules @@ -42,7 +43,7 @@ public class ClassNamingRule : StandardRule("class-naming") { node .takeIf { node.elementType == CLASS || node.elementType == OBJECT_DECLARATION } ?.findChildByType(IDENTIFIER) - ?.takeUnless { it.isValidFunctionName() || it.isTestClass() } + ?.takeUnless { it.isValidFunctionName() || it.isTestClass() || it.isTokenKeywordBetweenBackticks() } ?.let { emit(it.startOffset, "Class or object name should start with an uppercase letter and use camel case", false) } @@ -54,9 +55,25 @@ public class ClassNamingRule : StandardRule("class-naming") { private fun ASTNode.hasBackTickedIdentifier() = text.matches(BACK_TICKED_FUNCTION_NAME_REGEXP) + private fun ASTNode.isTokenKeywordBetweenBackticks() = + this + .takeIf { it.elementType == IDENTIFIER } + ?.text + .orEmpty() + .removeSurrounding("`") + .let { KEYWORDS.contains(it) } + private companion object { val VALID_CLASS_NAME_REGEXP = "[A-Z][A-Za-z\\d]*".regExIgnoringDiacriticsAndStrokesOnLetters() val BACK_TICKED_FUNCTION_NAME_REGEXP = Regex("`.*`") + private val KEYWORDS = + setOf(KtTokens.KEYWORDS, KtTokens.SOFT_KEYWORDS) + .flatMap { tokenSet -> tokenSet.types.mapNotNull { it.debugName } } + .filterNot { keyword -> + // The keyword sets contain a few 'keywords' which should be ignored. All valid keywords only contain lowercase + // characters + keyword.any { it.isUpperCase() } + }.toSet() } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRule.kt index 6faf9cccfc..4c7c2a9524 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRule.kt @@ -11,6 +11,7 @@ import com.pinterest.ktlint.rule.engine.core.api.ElementType.IMPORT_DIRECTIVE import com.pinterest.ktlint.rule.engine.core.api.ElementType.MODIFIER_LIST import com.pinterest.ktlint.rule.engine.core.api.ElementType.OVERRIDE_KEYWORD import com.pinterest.ktlint.rule.engine.core.api.ElementType.REFERENCE_EXPRESSION +import com.pinterest.ktlint.rule.engine.core.api.ElementType.TYPE_REFERENCE import com.pinterest.ktlint.rule.engine.core.api.ElementType.USER_TYPE import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER_LIST import com.pinterest.ktlint.rule.engine.core.api.RuleId @@ -26,6 +27,7 @@ import com.pinterest.ktlint.ruleset.standard.StandardRule import com.pinterest.ktlint.ruleset.standard.rules.internal.regExIgnoringDiacriticsAndStrokesOnLetters import org.ec4j.core.model.PropertyType import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.KtImportDirective @@ -68,14 +70,11 @@ public class FunctionNamingRule : node.isAnonymousFunction() || node.isOverrideFunction() || node.isAnnotatedWithAnyOf(ignoreWhenAnnotatedWith) - }?.let { - val identifierOffset = - node - .findChildByType(IDENTIFIER) - ?.startOffset - ?: 1 + }?.findChildByType(IDENTIFIER) + ?.takeUnless { it.isTokenKeywordBetweenBackticks() } + ?.let { identifier -> emit( - identifierOffset, + identifier.startOffset, "Function name should start with a lowercase letter (except factory methods) and use camel case", false, ) @@ -165,12 +164,20 @@ public class FunctionNamingRule : private fun ASTNode.annotationEntryName() = findChildByType(ElementType.CONSTRUCTOR_CALLEE) - ?.findChildByType(ElementType.TYPE_REFERENCE) - ?.findChildByType(ElementType.USER_TYPE) - ?.findChildByType(ElementType.REFERENCE_EXPRESSION) + ?.findChildByType(TYPE_REFERENCE) + ?.findChildByType(USER_TYPE) + ?.findChildByType(REFERENCE_EXPRESSION) ?.findChildByType(IDENTIFIER) ?.text + private fun ASTNode.isTokenKeywordBetweenBackticks() = + this + .takeIf { it.elementType == IDENTIFIER } + ?.text + .orEmpty() + .removeSurrounding("`") + .let { KEYWORDS.contains(it) } + public companion object { public val IGNORE_WHEN_ANNOTATED_WITH_PROPERTY: EditorConfigProperty> = EditorConfigProperty( @@ -185,6 +192,14 @@ public class FunctionNamingRule : private val VALID_FUNCTION_NAME_REGEXP = "[a-z][A-Za-z\\d]*".regExIgnoringDiacriticsAndStrokesOnLetters() private val VALID_TEST_FUNCTION_NAME_REGEXP = "(`.*`)|([a-z][A-Za-z\\d_]*)".regExIgnoringDiacriticsAndStrokesOnLetters() + private val KEYWORDS = + setOf(KtTokens.KEYWORDS, KtTokens.SOFT_KEYWORDS) + .flatMap { tokenSet -> tokenSet.types.mapNotNull { it.debugName } } + .filterNot { keyword -> + // The keyword sets contain a few 'keywords' which should be ignored. All valid keywords only contain lowercase + // characters + keyword.any { it.isUpperCase() } + }.toSet() private val TEST_LIBRARIES_SET = setOf( "io.kotest", diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule.kt index d1e76a5e95..a2d4e1d39b 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule.kt @@ -26,6 +26,7 @@ import com.pinterest.ktlint.ruleset.standard.StandardRule import com.pinterest.ktlint.ruleset.standard.rules.internal.regExIgnoringDiacriticsAndStrokesOnLetters import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType +import org.jetbrains.kotlin.lexer.KtTokens /** * https://kotlinlang.org/docs/coding-conventions.html#function-names @@ -50,6 +51,7 @@ public class PropertyNamingRule : StandardRule("property-naming") { ) { property .findChildByType(IDENTIFIER) + ?.takeUnless { it.isTokenKeywordBetweenBackticks() } ?.let { identifier -> when { property.hasConstModifier() -> { @@ -187,11 +189,27 @@ public class PropertyNamingRule : StandardRule("property-naming") { !hasModifier(PROTECTED_KEYWORD) && !hasModifier(INTERNAL_KEYWORD) + private fun ASTNode.isTokenKeywordBetweenBackticks() = + this + .takeIf { it.elementType == IDENTIFIER } + ?.text + .orEmpty() + .removeSurrounding("`") + .let { KEYWORDS.contains(it) } + private companion object { val LOWER_CAMEL_CASE_REGEXP = "[a-z][a-zA-Z0-9]*".regExIgnoringDiacriticsAndStrokesOnLetters() val SCREAMING_SNAKE_CASE_REGEXP = "[A-Z][_A-Z0-9]*".regExIgnoringDiacriticsAndStrokesOnLetters() val BACKING_PROPERTY_LOWER_CAMEL_CASE_REGEXP = "_[a-z][a-zA-Z0-9]*".regExIgnoringDiacriticsAndStrokesOnLetters() const val SERIAL_VERSION_UID_PROPERTY_NAME = "serialVersionUID" + val KEYWORDS = + setOf(KtTokens.KEYWORDS, KtTokens.SOFT_KEYWORDS) + .flatMap { tokenSet -> tokenSet.types.mapNotNull { it.debugName } } + .filterNot { keyword -> + // The keyword sets contain a few 'keywords' which should be ignored. All valid keywords only contain lowercase + // characters + keyword.any { it.isUpperCase() } + }.toSet() } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ClassNamingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ClassNamingRuleTest.kt index 16c79dce52..bd382cc0cd 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ClassNamingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ClassNamingRuleTest.kt @@ -139,4 +139,25 @@ class ClassNamingRuleTest { """.trimIndent() classNamingRuleAssertThat(code).hasNoLintViolations() } + + @ParameterizedTest(name = "Keyword: {0}") + @Suppress("ktlint:standard:argument-list-wrapping") + @ValueSource( + strings = [ + "abstract", "actual", "annotation", "as", "break", "by", "catch", "class", "companion", "const", "constructor", "context", + "continue", "contract", "crossinline", "data", "delegate", "do", "dynamic", "else", "enum", "expect", "external", "false", + "field", "file", "final", "finally", "for", "fun", "get", "header", "if", "impl", "import", "in", "infix", "init", "inline", + "inner", "interface", "internal", "is", "lateinit", "noinline", "null", "object", "open", "operator", "out", "override", + "package", "param", "private", "property", "protected", "public", "receiver", "reified", "return", "sealed", "set", "setparam", + "super", "suspend", "tailrec", "this", "throw", "true", "try", "typealias", "typeof", "val", "value", "var", "vararg", "when", + "where", "while", + ], + ) + fun `Issue 2352 - Given a keyword then allow it to be wrapped between backticks`(keyword: String) { + val code = + """ + class `$keyword` + """.trimIndent() + classNamingRuleAssertThat(code).hasNoLintViolations() + } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRuleTest.kt index aabb1fb59e..ec426d08d1 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRuleTest.kt @@ -271,4 +271,25 @@ class FunctionNamingRuleTest { """.trimIndent() functionNamingRuleAssertThat(code).hasNoLintViolations() } + + @ParameterizedTest(name = "Keyword: {0}") + @Suppress("ktlint:standard:argument-list-wrapping") + @ValueSource( + strings = [ + "abstract", "actual", "annotation", "as", "break", "by", "catch", "class", "companion", "const", "constructor", "context", + "continue", "contract", "crossinline", "data", "delegate", "do", "dynamic", "else", "enum", "expect", "external", "false", + "field", "file", "final", "finally", "for", "fun", "get", "header", "if", "impl", "import", "in", "infix", "init", "inline", + "inner", "interface", "internal", "is", "lateinit", "noinline", "null", "object", "open", "operator", "out", "override", + "package", "param", "private", "property", "protected", "public", "receiver", "reified", "return", "sealed", "set", "setparam", + "super", "suspend", "tailrec", "this", "throw", "true", "try", "typealias", "typeof", "val", "value", "var", "vararg", "when", + "where", "while", + ], + ) + fun `Issue 2352 - Given a keyword then allow it to be wrapped between backticks`(keyword: String) { + val code = + """ + fun `$keyword`() = "foo" + """.trimIndent() + functionNamingRuleAssertThat(code).hasNoLintViolations() + } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRuleTest.kt index 204c70e116..16c7f10ad4 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRuleTest.kt @@ -329,6 +329,31 @@ class PropertyNamingRuleTest { } } + @ParameterizedTest(name = "Keyword: {0}") + @Suppress("ktlint:standard:argument-list-wrapping") + @ValueSource( + strings = [ + "abstract", "actual", "annotation", "as", "break", "by", "catch", "class", "companion", "const", "constructor", "context", + "continue", "contract", "crossinline", "data", "delegate", "do", "dynamic", "else", "enum", "expect", "external", "false", + "field", "file", "final", "finally", "for", "fun", "get", "header", "if", "impl", "import", "in", "infix", "init", "inline", + "inner", "interface", "internal", "is", "lateinit", "noinline", "null", "object", "open", "operator", "out", "override", + "package", "param", "private", "property", "protected", "public", "receiver", "reified", "return", "sealed", "set", "setparam", + "super", "suspend", "tailrec", "this", "throw", "true", "try", "typealias", "typeof", "val", "value", "var", "vararg", "when", + "where", "while", + ], + ) + fun `Issue 2352 - Given a keyword then allow it to be wrapped between backticks`(keyword: String) { + val code = + """ + var `$keyword` = "some-value" + fun foo() { + var `$keyword` = "some-value" + val `$keyword` = "some-value" + } + """.trimIndent() + propertyNamingRuleAssertThat(code).hasNoLintViolations() + } + @KtlintDocumentationTest fun `Ktlint allowed examples`() { val code = From 356392e956a3d91b9f61dd844513efe8e74be2b0 Mon Sep 17 00:00:00 2001 From: paul-dingemans Date: Sat, 2 Dec 2023 16:40:17 +0100 Subject: [PATCH 2/2] Add examples to docs --- documentation/snapshot/docs/rules/standard.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/documentation/snapshot/docs/rules/standard.md b/documentation/snapshot/docs/rules/standard.md index d8c7b2894a..3e82373db4 100644 --- a/documentation/snapshot/docs/rules/standard.md +++ b/documentation/snapshot/docs/rules/standard.md @@ -346,6 +346,8 @@ Enforce naming of class and objects. class Foo class Foo1 + + class `class` // Any keyword is allowed when wrapped between backticks ``` === "[:material-heart:](#) Ktlint JUnit Test" @@ -384,6 +386,8 @@ Enforce naming of function. fun foo() {} fun fooBar() {} + + fun `fun` {} // Any keyword is allowed when wrapped between backticks ``` === "[:material-heart:](#) Ktlint Test" @@ -469,6 +473,8 @@ Enforce naming of property. val FOO1 = Foo() // In case developer want to communicate that Foo is deeply immutable } } + + var `package` = "foo" // Any keyword is allowed when wrapped between backticks ``` === "[:material-heart-off-outline:](#) Disallowed"