From 3f81397a4f91fb703d0c07012a19f46d9266a91a Mon Sep 17 00:00:00 2001 From: Ivo Smid Date: Sun, 21 Mar 2021 16:19:18 +0100 Subject: [PATCH] Detect filename is not compliant with PascalCase convention --- CHANGELOG.md | 1 + .../ktlint/ruleset/standard/FilenameRule.kt | 83 ++++++++------ .../ruleset/standard/FilenameRuleTest.kt | 108 ++++++++++++++++++ 3 files changed, 160 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68ee2747a4..461b1d3143 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed ### Changed +- Detect FilenameRule is not compliant with PascalCase convention ([#1004](https://github.com/pinterest/ktlint/pull/1117)) ### Removed ## [0.41.0] - 2021-03-16 diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRule.kt index 61c165343f..65063c03db 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRule.kt @@ -4,6 +4,7 @@ import com.pinterest.ktlint.core.KtLint import com.pinterest.ktlint.core.Rule import com.pinterest.ktlint.core.ast.ElementType.BLOCK_COMMENT import com.pinterest.ktlint.core.ast.ElementType.CLASS +import com.pinterest.ktlint.core.ast.ElementType.DOT import com.pinterest.ktlint.core.ast.ElementType.EOL_COMMENT import com.pinterest.ktlint.core.ast.ElementType.FILE_ANNOTATION_LIST import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER @@ -24,17 +25,6 @@ import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType */ class FilenameRule : Rule("filename"), Rule.Modifier.RestrictToRoot { - private val ignoreSet = setOf( - FILE_ANNOTATION_LIST, - PACKAGE_DIRECTIVE, - IMPORT_LIST, - WHITE_SPACE, - EOL_COMMENT, - BLOCK_COMMENT, - KDOC, - SHEBANG_COMMENT - ) - override fun visit( node: ASTNode, autoCorrect: Boolean, @@ -45,31 +35,60 @@ class FilenameRule : Rule("filename"), Rule.Modifier.RestrictToRoot { // ignore all non ".kt" files (including ".kts") return } - var type: String? = null - var className: String? = null - for (el in node.getChildren(null)) { - if (el.elementType == CLASS || + + val elements = node.getChildren(null).filterNotNull().filter { el -> + el.elementType == CLASS || el.elementType == OBJECT_DECLARATION || - el.elementType == TYPEALIAS - ) { - if (className != null) { - // more than one class/object/typealias present - return - } - val id = el.findChildByType(IDENTIFIER) - type = id?.prevCodeSibling()?.text - className = id?.text - } else if (!ignoreSet.contains(el.elementType)) { + el.elementType == TYPEALIAS || // https://github.com/android/android-ktx/blob/51005889235123f41492eaaecde3c623473dfe95/src/main/java/androidx/core/graphics/Path.kt case - return - } + !ignoreSet.contains(el.elementType) + }.map { el -> + val id = el.findChildByType(IDENTIFIER) + val prevCodeSibling = id?.prevCodeSibling() + // in case of extension function rename DOT to fun + val type = if (prevCodeSibling?.elementType == DOT) "fun" else prevCodeSibling?.text ?: "" + val name = id?.text ?: "" + ResultElement(type, name) } - if (className != null) { - val unescapedClassName = className.replace("`", "") - val name = Paths.get(filePath).fileName.toString().substringBefore(".") - if (name != "package" && name != unescapedClassName) { - emit(0, "$type $className should be declared in a file named $unescapedClassName.kt", false) + + val name = Paths.get(filePath).fileName.toString().substringBefore(".") + if (name != "package") { + if (elements.size == 1) { + val element = elements.first() + if (element.type != "fun") { + val (type, className) = element + val unescapedClassName = className.replace("`", "") + if (name != unescapedClassName) { + emit(0, "$type $className should be declared in a file named $unescapedClassName.kt", false) + return + } + } + } + + // in all other cases (multiple elements, single non-class element, ...) check filename for PascalCase + if (!pascalCase.matches(name)) { + emit(0, "File name $name.kt should conform PascalCase", false) } } } + + private data class ResultElement( + val type: String, + val name: String + ) + + companion object { + private val ignoreSet = setOf( + FILE_ANNOTATION_LIST, + PACKAGE_DIRECTIVE, + IMPORT_LIST, + WHITE_SPACE, + EOL_COMMENT, + BLOCK_COMMENT, + KDOC, + SHEBANG_COMMENT + ) + + private val pascalCase = """^[A-Z][A-Za-z0-9]*$""".toRegex() + } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRuleTest.kt index c528168142..49754c6f4c 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/FilenameRuleTest.kt @@ -103,6 +103,114 @@ class FilenameRuleTest { ).isEmpty() } + @Test + fun testNonMatchingMultipleElementsWithNonPascalCaseFilename() { + for ( + src in listOf( + "class A", + "class `A`", + "data class A(val v: Int)", + "sealed class A", + "interface A", + "object A", + "enum class A {A}", + "typealias A = Set", + "fun A.f() {}" + ) + ) { + assertThat( + FilenameRule().lint( + "foo.kt", + """ + class Bar + $src + """.trimIndent(), + ) + ).isEqualTo( + listOf( + LintError(1, 1, "filename", "File name foo.kt should conform PascalCase") + ) + ) + } + } + + @Test + fun testNonMatchingMultipleElementsWithNonPascalCaseFilenamex() { + for ( + src in listOf( + "fun A.f() {}", + "fun f() {}" + ) + ) { + assertThat( + FilenameRule().lint( + "foo.kt", + """ + $src + """.trimIndent(), + ) + ).isEqualTo( + listOf( + LintError(1, 1, "filename", "File name foo.kt should conform PascalCase") + ) + ) + } + } + + @Test + fun testMultipleElementsWithNonPascalCaseFilename() { + for ( + src in listOf( + "class A", + "class `A`", + "data class A(val v: Int)", + "sealed class A", + "interface A", + "object A", + "enum class A {A}", + "typealias A = Set", + "fun A.f() {}" + ) + ) { + assertThat( + FilenameRule().lint( + "Foo.kt", + """ + class Bar + $src + """.trimIndent(), + ) + ).isEmpty() + } + } + + @Test + fun testMultipleElementsInPackageKtFile() { + for ( + src in listOf( + "class A", + "class `A`", + "data class A(val v: Int)", + "sealed class A", + "interface A", + "object A", + "enum class A {A}", + "typealias A = Set", + "fun A.f() {}" + ) + ) { + assertThat( + FilenameRule().lint( + "package.kt", + """ + class Bar + $src + """.trimIndent(), + ) + ).isEmpty() + } + } + @Test fun testMultipleNonTopLevelClasses() { assertThat(