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 0000000000..9622c2bfe2 --- /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 84287e4c44..15aa1a2ad5 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 7973044d50..3b0e1b8f4c 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 dc75a4ae06..96c41bdd65 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 @@ -115,11 +115,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 17a2ddd8fa..57cf907a10 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 087a1934d1..1d64c651f8 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) = 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 List.head() = this.subList(0, this.size - 1) internal fun List.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 0000000000..e57759b8c4 --- /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 similarity index 100% rename from ktlint-test/src/main/kotlin/com/github/shyiko/ktlint/test/package.kt rename to ktlint-test/src/main/kotlin/com/github/shyiko/ktlint/test/DumpAST.kt