Skip to content

Commit

Permalink
Detect filename is not compliant with PascalCase convention
Browse files Browse the repository at this point in the history
  • Loading branch information
bedla committed Mar 21, 2021
1 parent 503b722 commit 3f81397
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,17 +25,6 @@ import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType
*/
class FilenameRule : Rule("filename"), Rule.Modifier.RestrictToRoot {

private val ignoreSet = setOf<IElementType>(
FILE_ANNOTATION_LIST,
PACKAGE_DIRECTIVE,
IMPORT_LIST,
WHITE_SPACE,
EOL_COMMENT,
BLOCK_COMMENT,
KDOC,
SHEBANG_COMMENT
)

override fun visit(
node: ASTNode,
autoCorrect: Boolean,
Expand All @@ -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<IElementType>(
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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Network.Node>",
"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<Network.Node>",
"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<Network.Node>",
"fun A.f() {}"
)
) {
assertThat(
FilenameRule().lint(
"package.kt",
"""
class Bar
$src
""".trimIndent(),
)
).isEmpty()
}
}

@Test
fun testMultipleNonTopLevelClasses() {
assertThat(
Expand Down

0 comments on commit 3f81397

Please sign in to comment.