diff options
author | Stanley Shyiko <stanley.shyiko@gmail.com> | 2018-05-01 09:42:10 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-05-01 09:42:10 -0700 |
commit | 79f5a7991a5f367e2a55b8caecfa63464b68ae7c (patch) | |
tree | bed13d936e1b7d6894b87d535c3c7d5c14a0ca24 | |
parent | cb91be53736d57fdfc6efd81aa6c6bff2d6fc92f (diff) | |
parent | 1e3823cf77c4be8ab165814a13d2a986c6024da2 (diff) | |
download | ktlint-79f5a7991a5f367e2a55b8caecfa63464b68ae7c.tar.gz |
Merge pull request #194 from JelloRanger/jelloranger/add-single-class-matches-filename-rule
Add rule to check that a single top level class name matches the file name
8 files changed, 126 insertions, 15 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 new file mode 100644 index 00000000..9622c2bf --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ClassNameMatchesFileNameRule.kt @@ -0,0 +1,39 @@ +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/NoMultipleSpacesRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoMultipleSpacesRule.kt index 84287e4c..15aa1a2a 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoMultipleSpacesRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoMultipleSpacesRule.kt @@ -78,9 +78,4 @@ class NoMultipleSpacesRule : Rule("no-multi-spaces") { } } } - - private fun ASTNode.visit(cb: (node: ASTNode) -> Unit) { - cb(this) - this.getChildren(null).forEach { it.visit(cb) } - } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRule.kt index 7973044d..3b0e1b8f 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/NoUnusedImportsRule.kt @@ -84,9 +84,4 @@ class NoUnusedImportsRule : Rule("no-unused-imports") { } private fun String.isComponentN() = componentNRegex.matches(this) - - private fun ASTNode.visit(cb: (node: ASTNode) -> Unit) { - cb(this) - this.getChildren(null).forEach { it.visit(cb) } - } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ParameterListWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ParameterListWrappingRule.kt index 3117727b..aa4e2843 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ParameterListWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/ParameterListWrappingRule.kt @@ -121,11 +121,6 @@ class ParameterListWrappingRule : Rule("parameter-list-wrapping") { return offsetToTheLeft + 1 } - private fun ASTNode.visit(cb: (node: ASTNode) -> Unit) { - cb(this) - this.getChildren(null).forEach { it.visit(cb) } - } - private fun errorMessage(node: ASTNode) = when (node.elementType) { KtStubElementTypes.VALUE_PARAMETER -> 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 17a2ddd8..57cf907a 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 @@ -7,6 +7,7 @@ class StandardRuleSetProvider : RuleSetProvider { override fun get(): RuleSet = RuleSet("standard", ChainWrappingRule(), + ClassNameMatchesFileNameRule(), FinalNewlineRule(), // disabled until it's clear how to reconcile difference in Intellij & Android Studio import layout // ImportOrderingRule(), diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/package.kt b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/package.kt index 087a1934..1d64c651 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/package.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/github/shyiko/ktlint/ruleset/standard/package.kt @@ -1,5 +1,6 @@ package com.github.shyiko.ktlint.ruleset.standard +import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil @@ -11,6 +12,10 @@ internal fun PsiElement.isPartOf(clazz: KClass<out PsiElement>) = getNonStrictPa internal fun PsiElement.isPartOfString() = isPartOf(KtStringTemplateEntry::class) internal fun PsiElement.prevLeaf(): LeafPsiElement? = PsiTreeUtil.prevLeaf(this) as LeafPsiElement? internal fun PsiElement.nextLeaf(): LeafPsiElement? = PsiTreeUtil.nextLeaf(this) as LeafPsiElement? +internal fun ASTNode.visit(cb: (node: ASTNode) -> Unit) { + cb(this) + this.getChildren(null).forEach { it.visit(cb) } +} internal fun <T> List<T>.head() = this.subList(0, this.size - 1) internal fun <T> List<T>.tail() = this.subList(1, this.size) 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 new file mode 100644 index 00000000..e57759b8 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/github/shyiko/ktlint/ruleset/standard/ClassNameMatchesFileNameRuleTest.kt @@ -0,0 +1,81 @@ +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-test/src/main/kotlin/com/github/shyiko/ktlint/test/package.kt b/ktlint-test/src/main/kotlin/com/github/shyiko/ktlint/test/DumpAST.kt index a22223ee..a22223ee 100644 --- a/ktlint-test/src/main/kotlin/com/github/shyiko/ktlint/test/package.kt +++ b/ktlint-test/src/main/kotlin/com/github/shyiko/ktlint/test/DumpAST.kt |