Skip to content

Commit

Permalink
Allow property, function and class name to be same as keyword wrapped…
Browse files Browse the repository at this point in the history
… with backticks (#2405)

* Allow property, function and class name to be same as keyword wrapped with backticks

Closes #2352
  • Loading branch information
paul-dingemans authored Dec 3, 2023
1 parent 07a43f4 commit c670818
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 11 deletions.
6 changes: 6 additions & 0 deletions documentation/snapshot/docs/rules/standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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<Set<String>> =
EditorConfigProperty(
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -50,6 +51,7 @@ public class PropertyNamingRule : StandardRule("property-naming") {
) {
property
.findChildByType(IDENTIFIER)
?.takeUnless { it.isTokenKeywordBetweenBackticks() }
?.let { identifier ->
when {
property.hasConstModifier() -> {
Expand Down Expand Up @@ -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()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down

0 comments on commit c670818

Please sign in to comment.