Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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