diff options
author | Stanley Shyiko <stanley.shyiko@gmail.com> | 2018-05-02 18:43:43 -0700 |
---|---|---|
committer | Stanley Shyiko <stanley.shyiko@gmail.com> | 2018-05-02 18:43:43 -0700 |
commit | 003fe04879adf5c70ec50be22cb50c033224271f (patch) | |
tree | 742089bb3dcedd54da158e235c12f71b4ba40bcc | |
parent | 32d3132791d33a1eb11fd317912a15703363e75f (diff) | |
download | ktlint-003fe04879adf5c70ec50be22cb50c033224271f.tar.gz |
Improved filename rule (#194)
5 files changed, 193 insertions, 121 deletions
diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ClassNameMatchesFileNameRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ClassNameMatchesFileNameRule.kt deleted file mode 100644 index 9622c2bf..00000000 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ClassNameMatchesFileNameRule.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.github.shyiko.ktlint.ruleset.standard - -import com.github.shyiko.ktlint.core.KtLint -import com.github.shyiko.ktlint.core.Rule -import org.jetbrains.kotlin.com.intellij.lang.ASTNode -import org.jetbrains.kotlin.lexer.KtTokens -import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes -import java.nio.file.Paths - -/** - * If there is only one top level class in a given file, then its name should match the file's name - */ -class ClassNameMatchesFileNameRule : Rule("class-name-matches-file-name"), Rule.Modifier.RestrictToRoot { - - override fun visit( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit - ) { - val filePath = node.getUserData(KtLint.FILE_PATH_USER_DATA_KEY) - - // Ignore all non ".kt" files (including ".kts") - if (filePath?.endsWith(".kt") != true) { - return - } - - val topLevelClassNames = node.getChildren(null) - .filter { it.elementType == KtStubElementTypes.CLASS } - .mapNotNull { it.findChildByType(KtTokens.IDENTIFIER)?.text } - - val name = Paths.get(filePath).fileName.toString().substringBefore(".") - if (topLevelClassNames.size == 1 && name != topLevelClassNames.first()) { - val className = topLevelClassNames.first() - emit(0, - "Class $className should be declared in a file named $className.kt", - false) - } - } -} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FilenameRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FilenameRule.kt new file mode 100644 index 00000000..8c7169c8 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/FilenameRule.kt @@ -0,0 +1,63 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.KtLint +import com.github.shyiko.ktlint.core.Rule +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespaceAndComments +import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes +import java.nio.file.Paths + +/** + * If there is only one top level class/object/typealias in a given file, then its name should match the file's name. + */ +class FilenameRule : Rule("filename"), Rule.Modifier.RestrictToRoot { + + private val ignoreSet = setOf<IElementType>( + KtStubElementTypes.FILE_ANNOTATION_LIST, + KtStubElementTypes.PACKAGE_DIRECTIVE, + KtStubElementTypes.IMPORT_LIST, + KtTokens.WHITE_SPACE, + KtTokens.EOL_COMMENT, + KtTokens.BLOCK_COMMENT, + KtTokens.DOC_COMMENT, + KtTokens.SHEBANG_COMMENT + ) + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + val filePath = node.getUserData(KtLint.FILE_PATH_USER_DATA_KEY) + if (filePath?.endsWith(".kt") != true) { + // 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 == KtStubElementTypes.CLASS || + el.elementType == KtStubElementTypes.OBJECT_DECLARATION || + el.elementType == KtStubElementTypes.TYPEALIAS) { + if (className != null) { + // more than one class/object/typealias present + return + } + val id = el.findChildByType(KtTokens.IDENTIFIER) + type = id?.psi?.getPrevSiblingIgnoringWhitespaceAndComments(false)?.text + className = id?.text + } else if (!ignoreSet.contains(el.elementType)) { + // https://github.com/android/android-ktx/blob/master/src/main/java/androidx/core/graphics/Path.kt case + return + } + } + if (className != null) { + val name = Paths.get(filePath).fileName.toString().substringBefore(".") + if (name != "package" && name != className) { + emit(0, "$type $className should be declared in a file named $className.kt", false) + } + } + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StandardRuleSetProvider.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StandardRuleSetProvider.kt index 39ba666c..87f06b42 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StandardRuleSetProvider.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/StandardRuleSetProvider.kt @@ -8,7 +8,7 @@ class StandardRuleSetProvider : RuleSetProvider { override fun get(): RuleSet = RuleSet("standard", ChainWrappingRule(), CommentSpacingRule(), - ClassNameMatchesFileNameRule(), + FilenameRule(), FinalNewlineRule(), // disabled until it's clear how to reconcile difference in Intellij & Android Studio import layout // ImportOrderingRule(), diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/ClassNameMatchesFileNameRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/ClassNameMatchesFileNameRuleTest.kt deleted file mode 100644 index e57759b8..00000000 --- a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/ClassNameMatchesFileNameRuleTest.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.github.shyiko.ktlint.ruleset.standard - -import com.github.shyiko.ktlint.core.LintError -import com.github.shyiko.ktlint.test.lint -import org.assertj.core.api.Assertions.assertThat -import org.testng.annotations.Test - -class ClassNameMatchesFileNameRuleTest { - - @Test - fun testMatchingSingleClassName() { - assertThat(ClassNameMatchesFileNameRule().lint( - """ - class A - """.trimIndent(), - fileName("/some/path/A.kt") - )).isEmpty() - } - - @Test - fun testNonMatchingSingleClassName() { - assertThat(ClassNameMatchesFileNameRule().lint( - """ - class B - """.trimIndent(), - fileName("A.kt") - )).isEqualTo(listOf( - LintError(1, 1, "class-name-matches-file-name", "Class B should be declared in a file named B.kt") - )) - } - - @Test - fun testMultipleTopLevelClasses() { - assertThat(ClassNameMatchesFileNameRule().lint( - """ - class B - class C - """.trimIndent(), - fileName("A.kt") - )).isEmpty() - } - - @Test - fun testMultipleNonTopLevelClasses() { - assertThat(ClassNameMatchesFileNameRule().lint( - """ - class B { - class C - class D - } - """.trimIndent(), - fileName("A.kt") - )).isEqualTo(listOf( - LintError(1, 1, "class-name-matches-file-name", "Class B should be declared in a file named B.kt") - )) - } - - @Test - fun testCaseSensitiveMatching() { - assertThat(ClassNameMatchesFileNameRule().lint( - """ - interface Woohoo - """.trimIndent(), - fileName("woohoo.kt") - )).isEqualTo(listOf( - LintError(1, 1, "class-name-matches-file-name", "Class Woohoo should be declared in a file named Woohoo.kt") - )) - } - - @Test - fun testIgnoreKotlinScriptFiles() { - assertThat(ClassNameMatchesFileNameRule().lint( - """ - class B - """.trimIndent(), - fileName("A.kts") - )).isEmpty() - } - - private fun fileName(fileName: String) = mapOf("file_path" to fileName) -} diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/FilenameRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/FilenameRuleTest.kt new file mode 100644 index 00000000..84e7ad4a --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/FilenameRuleTest.kt @@ -0,0 +1,129 @@ +package com.github.shyiko.ktlint.ruleset.standard + +import com.github.shyiko.ktlint.core.LintError +import com.github.shyiko.ktlint.test.lint +import org.assertj.core.api.Assertions.assertThat +import org.testng.annotations.Test + +class FilenameRuleTest { + + @Test + fun testMatchingSingleClassName() { + for (src in listOf( + "class A", + "data class A(val v: Int)", + "sealed class A", + "interface A", + "object A", + "enum class A {A}", + "typealias A = Set<Network.Node>", + // >1 declaration case + "class B\nfun A.f() {}" + )) { + assertThat(FilenameRule().lint( + """ + /* + * license + */ + @file:JvmName("Foo") + package x + import y.Z + $src + // + """.trimIndent(), + fileName("/some/path/A.kt") + )).isEmpty() + } + } + + @Test + fun testNonMatchingSingleClassName() { + for (src in mapOf( + "class A" to "class", + "data class A(val v: Int)" to "class", + "sealed class A" to "class", + "interface A" to "interface", + "object A" to "object", + "enum class A {A}" to "class", + "typealias A = Set<Network.Node>" to "typealias" + )) { + assertThat(FilenameRule().lint( + """ + /* + * license + */ + @file:JvmName("Foo") + package x + import y.Z + ${src.key} + // + """.trimIndent(), + fileName("/some/path/B.kt") + )).isEqualTo(listOf( + LintError(1, 1, "filename", "${src.value} A should be declared in a file named A.kt") + )) + } + } + + @Test + fun testFileWithoutTopLevelDeclarations() { + assertThat(FilenameRule().lint( + """ + /* + * copyright + */ + """.trimIndent(), + fileName("A.kt") + )).isEmpty() + } + + @Test + fun testMultipleTopLevelClasses() { + assertThat(FilenameRule().lint( + """ + class B + class C + """.trimIndent(), + fileName("A.kt") + )).isEmpty() + } + + @Test + fun testMultipleNonTopLevelClasses() { + assertThat(FilenameRule().lint( + """ + class B { + class C + class D + } + """.trimIndent(), + fileName("A.kt") + )).isEqualTo(listOf( + LintError(1, 1, "filename", "class B should be declared in a file named B.kt") + )) + } + + @Test + fun testCaseSensitiveMatching() { + assertThat(FilenameRule().lint( + """ + interface Woohoo + """.trimIndent(), + fileName("woohoo.kt") + )).isEqualTo(listOf( + LintError(1, 1, "filename", "interface Woohoo should be declared in a file named Woohoo.kt") + )) + } + + @Test + fun testIgnoreKotlinScriptFiles() { + assertThat(FilenameRule().lint( + """ + class B + """.trimIndent(), + fileName("A.kts") + )).isEmpty() + } + + private fun fileName(fileName: String) = mapOf("file_path" to fileName) +} |