diff --git a/build.gradle b/build.gradle index cb63563458..7f6c4eaf4e 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,7 @@ ext.deps = [ 'compiler': "org.jetbrains.kotlin:kotlin-compiler-embeddable:${versions.kotlin}" ], 'klob' : 'com.github.shyiko.klob:klob:0.2.1', + ec4j : 'org.ec4j.core:ec4j-core:0.2.0', 'aether' : [ 'api' : "org.eclipse.aether:aether-api:${versions.aether}", 'spi' : "org.eclipse.aether:aether-spi:${versions.aether}", diff --git a/ktlint-core/build.gradle b/ktlint-core/build.gradle index d3fa3f12f6..bb6a23eb1f 100644 --- a/ktlint-core/build.gradle +++ b/ktlint-core/build.gradle @@ -6,6 +6,7 @@ plugins { dependencies { implementation deps.kotlin.stdlib implementation deps.kotlin.compiler + implementation deps.ec4j testImplementation deps.junit testImplementation deps.assertj diff --git a/ktlint-core/pom.xml b/ktlint-core/pom.xml index e4f8a7a507..94a722e949 100644 --- a/ktlint-core/pom.xml +++ b/ktlint-core/pom.xml @@ -23,6 +23,11 @@ kotlin-compiler-embeddable ${kotlin.version} + + org.ec4j.core + ec4j-core + ${ec4j.version} + junit junit diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt index dc652b5fc8..d711633bbe 100644 --- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt @@ -230,7 +230,10 @@ object KtLint { node.putUserData(FILE_PATH_USER_DATA_KEY, userData["file_path"]) node.putUserData(EDITOR_CONFIG_USER_DATA_KEY, EditorConfig.fromMap(editorConfigMap - "android" - "file_path")) node.putUserData(ANDROID_USER_DATA_KEY, android) - node.putUserData(DISABLED_RULES, userData["disabled_rules"]?.split(",")?.toSet() ?: emptySet()) + node.putUserData( + DISABLED_RULES, + userData["disabled_rules"]?.split(",")?.map { it.trim() }?.toSet() ?: emptySet() + ) } private fun visitor( diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigInternal.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigInternal.kt index 2bf29a9697..564cb79f62 100644 --- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigInternal.kt +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigInternal.kt @@ -1,12 +1,18 @@ package com.pinterest.ktlint.core.internal -import java.io.ByteArrayInputStream +import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths -import java.util.Properties import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap +import org.ec4j.core.PropertyTypeRegistry +import org.ec4j.core.Resource +import org.ec4j.core.model.EditorConfig +import org.ec4j.core.model.Version +import org.ec4j.core.parser.EditorConfigModelHandler +import org.ec4j.core.parser.EditorConfigParser +import org.ec4j.core.parser.ErrorHandler /** * This class handles traversing the filetree and parsing and merging the contents of any discovered .editorconfig files @@ -18,11 +24,10 @@ class EditorConfigInternal private constructor ( ) : Map by data { companion object : EditorConfigLookup { - override fun of(dir: String) = of(Paths.get(dir)) override fun of(dir: Path) = generateSequence(locate(dir)) { seed -> locate(seed.parent.parent) } // seed.parent == .editorconfig dir - .map { it to lazy { load(it) } } + .map { it to lazy { loadEditorconfigFile(it) } } .let { seq -> // stop when .editorconfig with "root = true" is found, go deeper otherwise var prev: Pair>>>? = null @@ -64,7 +69,7 @@ class EditorConfigInternal private constructor ( ( parent?.data ?: emptyMap() - ) + flatten(load(editorConfigPath)) + ) + flatten(loadEditorconfigFile(editorConfigPath)) ) } else { parent @@ -91,7 +96,7 @@ class EditorConfigInternal private constructor ( } } - private fun flatten(data: LinkedHashMap>): Map { + private fun flatten(data: Map>): Map { val map = mutableMapOf() val patternsToSearchFor = arrayOf("*", "*.kt", "*.kts") for ((sectionName, section) in data) { @@ -99,7 +104,7 @@ class EditorConfigInternal private constructor ( continue } val patterns = try { - parseSection(sectionName.substring(1, sectionName.length - 1)) + parseSection(sectionName) } catch (e: Exception) { throw RuntimeException( "ktlint failed to parse .editorconfig section \"$sectionName\"" + @@ -114,26 +119,37 @@ class EditorConfigInternal private constructor ( return map.toSortedMap() } - private fun load(path: Path) = - linkedMapOf>().also { map -> - object : Properties() { - private var section: MutableMap? = null + private fun parseEditorconfigFile(path: Path): EditorConfig { + val parser = EditorConfigParser.builder().build() + val handler = EditorConfigModelHandler(PropertyTypeRegistry.default_(), Version.CURRENT) - override fun put(key: Any, value: Any): Any? { - val sectionName = (key as String).trim() - if (sectionName.startsWith('[') && sectionName.endsWith(']') && value == "") { - section = mutableMapOf().also { map.put(sectionName, it) } - } else { - val section = - section - ?: mutableMapOf().also { section = it; map.put("", it) } - section[key] = value.toString() + parser.parse( + Resource.Resources.ofPath(path, StandardCharsets.UTF_8), + handler, + ErrorHandler.THROW_SYNTAX_ERRORS_IGNORE_OTHERS + ) + return handler.editorConfig + } + + private fun loadEditorconfigFile(editorConfigFile: Path): Map> { + val editorConfig = parseEditorconfigFile(editorConfigFile) + + var mapRepresentation = editorConfig.sections + .associate { section -> + section.glob.toString() to section + .properties + .mapValues { entry -> + entry.value.sourceValue } - return null - } - }.load(ByteArrayInputStream(Files.readAllBytes(path))) + } + + if (editorConfig.isRoot) { + mapRepresentation = mapRepresentation + ("" to mapOf("root" to true.toString())) } + return mapRepresentation + } + internal fun parseSection(sectionName: String): List { val result = mutableListOf() fun List>.collect0(i: Int = 0, str: Array, acc: MutableList) { diff --git a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigInternalTest.kt b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigInternalTest.kt index 6705ecc646..2eec608a54 100644 --- a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigInternalTest.kt +++ b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigInternalTest.kt @@ -2,105 +2,143 @@ package com.pinterest.ktlint.core.internal import com.google.common.jimfs.Configuration import com.google.common.jimfs.Jimfs +import java.nio.file.FileSystem import java.nio.file.Files import org.assertj.core.api.Assertions.assertThat +import org.junit.After import org.junit.Test class EditorConfigInternalTest { + private val tempFileSystem = Jimfs.newFileSystem(Configuration.forCurrentPlatform()) + + private fun FileSystem.writeEditorConfigFile( + filePath: String, + content: String + ) { + Files.createDirectories(getPath(filePath)) + Files.write(getPath("$filePath/.editorconfig"), content.toByteArray()) + } + + @After + fun tearDown() { + tempFileSystem.close() + } @Test fun testParentDirectoryFallback() { - val fs = Jimfs.newFileSystem(Configuration.unix()) - Files.createDirectories(fs.getPath("/projects/project-1/project-1-subdirectory")) - for ( - cfg in arrayOf( - """ - [*] - indent_size = 2 - """, - """ - root = true - [*] - indent_size = 2 - """, - """ - [*] - indent_size = 4 - [*.{kt,kts}] - indent_size = 2 - """, - """ - [*.{kt,kts}] - indent_size = 4 - [*] - indent_size = 2 - """ + val projectDir = "/projects/project-1" + val projectSubDirectory = "$projectDir/project-1-subdirectory" + Files.createDirectories(tempFileSystem.getPath(projectSubDirectory)) + val editorConfigFiles = arrayOf( + """ + [*] + indent_size = 2 + """.trimIndent(), + """ + root = true + [*] + indent_size = 2 + """.trimIndent(), + """ + [*] + indent_size = 4 + [*.{kt,kts}] + indent_size = 2 + """.trimIndent(), + """ + [*.{kt,kts}] + indent_size = 4 + [*] + indent_size = 2 + """.trimIndent() + ) + + editorConfigFiles.forEach { editorConfigFileContent -> + tempFileSystem.writeEditorConfigFile(projectDir, editorConfigFileContent) + + val editorConfig = EditorConfigInternal.of( + tempFileSystem.getPath(projectSubDirectory) ) - ) { - Files.write(fs.getPath("/projects/project-1/.editorconfig"), cfg.trimIndent().toByteArray()) - val editorConfig = EditorConfigInternal.of(fs.getPath("/projects/project-1/project-1-subdirectory")) + assertThat(editorConfig?.parent).isNull() assertThat(editorConfig?.toMap()) - .overridingErrorMessage("Expected \n%s\nto yield indent_size = 2", cfg.trimIndent()) - .isEqualTo(mapOf("indent_size" to "2")) + .overridingErrorMessage( + "Expected \n%s\nto yield indent_size = 2", + editorConfigFileContent + ) + .isEqualTo( + mapOf( + "indent_size" to "2", + "tab_width" to "2" + ) + ) } } @Test fun testRootTermination() { - val fs = Jimfs.newFileSystem(Configuration.unix()) - Files.createDirectories(fs.getPath("/projects/project-1/project-1-subdirectory")) - Files.write( - fs.getPath("/projects/.editorconfig"), + val rootDir = "/projects" + val project1Dir = "$rootDir/project-1" + val project1Subdirectory = "$project1Dir/project-1-subdirectory" + tempFileSystem.writeEditorConfigFile( + rootDir, """ root = true [*] end_of_line = lf - """.trimIndent().toByteArray() + """.trimIndent() ) - Files.write( - fs.getPath("/projects/project-1/.editorconfig"), + tempFileSystem.writeEditorConfigFile( + project1Dir, """ root = true [*.{kt,kts}] indent_size = 4 indent_style = space - """.trimIndent().toByteArray() + """.trimIndent() ) - Files.write( - fs.getPath("/projects/project-1/project-1-subdirectory/.editorconfig"), + tempFileSystem.writeEditorConfigFile( + project1Subdirectory, """ [*] indent_size = 2 - """.trimIndent().toByteArray() + """.trimIndent() ) - EditorConfigInternal.of(fs.getPath("/projects/project-1/project-1-subdirectory")).let { editorConfig -> - assertThat(editorConfig?.parent).isNotNull() - assertThat(editorConfig?.parent?.parent).isNull() - assertThat(editorConfig?.toMap()).isEqualTo( - mapOf( - "indent_size" to "2", - "indent_style" to "space" - ) + + var parsedEditorConfig = EditorConfigInternal.of( + tempFileSystem.getPath(project1Subdirectory) + ) + assertThat(parsedEditorConfig?.parent).isNotNull + assertThat(parsedEditorConfig?.parent?.parent).isNull() + assertThat(parsedEditorConfig?.toMap()).isEqualTo( + mapOf( + "indent_size" to "2", + "tab_width" to "2", + "indent_style" to "space" ) - } - EditorConfigInternal.of(fs.getPath("/projects/project-1")).let { editorConfig -> - assertThat(editorConfig?.parent).isNull() - assertThat(editorConfig?.toMap()).isEqualTo( - mapOf( - "indent_size" to "4", - "indent_style" to "space" - ) + ) + + parsedEditorConfig = EditorConfigInternal.of( + tempFileSystem.getPath(project1Dir) + ) + assertThat(parsedEditorConfig?.parent).isNull() + assertThat(parsedEditorConfig?.toMap()).isEqualTo( + mapOf( + "indent_size" to "4", + "tab_width" to "4", + "indent_style" to "space" ) - } - EditorConfigInternal.of(fs.getPath("/projects")).let { editorConfig -> - assertThat(editorConfig?.parent).isNull() - assertThat(editorConfig?.toMap()).isEqualTo( - mapOf( - "end_of_line" to "lf" - ) + ) + + parsedEditorConfig = EditorConfigInternal.of( + tempFileSystem.getPath(rootDir) + ) + assertThat(parsedEditorConfig?.parent).isNull() + assertThat(parsedEditorConfig?.toMap()).isEqualTo( + mapOf( + "end_of_line" to "lf" ) - } + ) } @Test @@ -123,4 +161,50 @@ class EditorConfigInternalTest { assertThat(EditorConfigInternal.parseSection("*.{js,{py")).isEqualTo(listOf("*.js", "*.{py")) assertThat(EditorConfigInternal.parseSection("*.py}")).isEqualTo(listOf("*.py}")) } + + @Test + fun `Should parse assignment with spaces`() { + val projectDir = "/project" + val editorconfigFile = + """ + [*.{kt, kts}] + insert_final_newline = true + disabled_rules = import-ordering + """.trimIndent() + tempFileSystem.writeEditorConfigFile(projectDir, editorconfigFile) + + val parsedEditorConfig = EditorConfigInternal.of( + tempFileSystem.getPath(projectDir) + ) + + assertThat(parsedEditorConfig).isNotNull + assertThat(parsedEditorConfig?.toMap()).isEqualTo( + mapOf( + "insert_final_newline" to "true", + "disabled_rules" to "import-ordering" + ) + ) + } + + @Test + fun `Should parse list with spaces after comma`() { + val projectDir = "/project" + val editorconfigFile = + """ + [*.{kt, kts}] + disabled_rules = import-ordering, no-wildcard-imports + """.trimIndent() + tempFileSystem.writeEditorConfigFile(projectDir, editorconfigFile) + + val parsedEditorConfig = EditorConfigInternal.of( + tempFileSystem.getPath(projectDir) + ) + + assertThat(parsedEditorConfig).isNotNull + assertThat(parsedEditorConfig?.toMap()).isEqualTo( + mapOf( + "disabled_rules" to "import-ordering, no-wildcard-imports" + ) + ) + } } diff --git a/pom.xml b/pom.xml index bd53c9f9a0..ff9a268661 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,7 @@ 1.1.0 3.3.9 3.9.6 + 0.2.0 4.12 3.12.2 1.1