From 54f819441520ca5ce0eb118dd9fc8afa03779e41 Mon Sep 17 00:00:00 2001 From: paul-dingemans Date: Sat, 13 Aug 2022 16:14:31 +0200 Subject: [PATCH] Improve support of default editorconfig properties Deprecate ExperimentalParams.editorConfigDefaults in favor of new parameter ExperimentalParams.editorConfigDefaults. When used in the old implementation this resulted in ignoring all ".editorconfig" files on the path to the file. The new implementation uses properties from the "editorConfigDefaults" parameter only when no ".editorconfig" files on the path to the file supplies this property for the filepath. Closes #1551 API consumers can easily create the EditConfigDefaults by calling "EditConfigDefaults.load(path)" or creating it programmatically. The CLI still supports the "--editorconfig=" option but has improved support. The path given can be either be a path to file or directory. In case of a directory path, it is expected that the directory does contain a file with name ".editorconfig". In of a file path, any valid file name is accepted. The path can be relative or absolute. Depending on the OS, the "~" at the start of the path is accepted as well. BaseCLITest no longer always waits 3 seconds for completion of the asynchronous process. Once the process is started, it checks every 100 ms whether the process is still alive (e.g. is running) and stops polling otherwise resulting in better performance (most notable on local machine). The maximum duration of the CLI test has been increased to 10 seconds. --- CHANGELOG.md | 9 + build.gradle | 13 +- .../com/pinterest/ktlint/core/KtLint.kt | 18 +- .../ktlint/core/api/EditorConfigDefaults.kt | 36 ++ .../core/api/UsesEditorConfigProperties.kt | 35 +- .../internal/EditorConfigDefaultsLoader.kt | 77 +++ .../core/internal/EditorConfigLoader.kt | 224 +++++-- .../ktlint/core/internal/PreparedCode.kt | 13 +- .../internal/ThreadSafeEditorConfigCache.kt | 12 +- .../EditorConfigDefaultsLoaderTest.kt | 138 +++++ .../core/internal/EditorConfigLoaderTest.kt | 563 ++++++++++++++++-- .../pinterest/ktlint/internal/FileUtils.kt | 17 +- .../ktlint/internal/KtlintCommandLine.kt | 23 +- .../ktlint/internal/PrintASTSubCommand.kt | 2 + .../com/pinterest/ktlint/BaseCLITest.kt | 43 +- .../EditorConfigDefaultsLoaderCLITest.kt | 107 ++++ .../editorconfig-path/project/.editorconfig | 7 + .../project/.editorconfig-bar | 4 + ...nfig-default-max-line-length-on-tests-only | 4 + .../.editorconfig-disable-filename-rule | 4 + .../project/editorconfig-alternative | 4 + .../project/src/main/kotlin/example/Bar.kts | 1 + .../project/src/main/kotlin/example/Foo.kt | 1 + .../src/main/kotlin/example/foobar1.kt | 1 + .../main/kotlin/filename-example/foobar2.kt | 1 + .../src/test/kotlin/example/BarTest.kts | 1 + .../src/test/kotlin/example/FooTest.kt | 1 + ktlint/src/test/resources/test-baseline.xml | 9 + 28 files changed, 1193 insertions(+), 175 deletions(-) create mode 100644 ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/api/EditorConfigDefaults.kt create mode 100644 ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigDefaultsLoader.kt create mode 100644 ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigDefaultsLoaderTest.kt create mode 100644 ktlint/src/test/kotlin/com/pinterest/ktlint/EditorConfigDefaultsLoaderCLITest.kt create mode 100644 ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig create mode 100644 ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig-bar create mode 100644 ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig-default-max-line-length-on-tests-only create mode 100644 ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig-disable-filename-rule create mode 100644 ktlint/src/test/resources/cli/editorconfig-path/project/editorconfig-alternative create mode 100644 ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/example/Bar.kts create mode 100644 ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/example/Foo.kt create mode 100644 ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/example/foobar1.kt create mode 100644 ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/filename-example/foobar2.kt create mode 100644 ktlint/src/test/resources/cli/editorconfig-path/project/src/test/kotlin/example/BarTest.kts create mode 100644 ktlint/src/test/resources/cli/editorconfig-path/project/src/test/kotlin/example/FooTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 050550acc9..cbe7f03f2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,6 +128,13 @@ The `.editorconfig` property `disabled_rules` (api property `DefaultEditorConfig Although, Ktlint 0.47.0 falls back on property `disabled_rules` whenever `ktlint_disabled_rules` is not found, this result in a warning message being printed. +#### Default/alternative .editorconfig + +Parameter "ExperimentalParams.editorConfigDefaults" is deprecated in favor of the new parameter "ExperimentalParams.editorConfigDefaults". When used in the old implementation this resulted in ignoring all ".editorconfig" files on the path to the file. The new implementation uses properties from the "editorConfigDefaults"parameter only when no ".editorconfig" files on the path to the file supplies this property for the filepath. + +API consumers can easily create the EditConfigDefaults by calling +"EditConfigDefaults.load(path)" or creating it programmatically. + ### Added * Add `format` reporter. This reporter prints a one-line-summary of the formatting status per file. ([#621](https://github.com/pinterest/ktlint/issue/621)). @@ -148,12 +155,14 @@ Although, Ktlint 0.47.0 falls back on property `disabled_rules` whenever `ktlint * Prevent class cast exception on ".editorconfig" property `ktlint_code_style` ([#1559](https://github.com/pinterest/ktlint/issues/1559)) * Handle trailing comma in enums `trailing-comma` ([#1542](https://github.com/pinterest/ktlint/pull/1542)) * Split rule `trailing-comma` into `trailing-comma-on-call-site` and `trailing-comma-on-declaration-site` ([#1555](https://github.com/pinterest/ktlint/pull/1555)) +* Support globs containing directories in the ".editorconfig" supplied via CLI "--editorconfig" ([#1551](https://github.com/pinterest/ktlint/pull/1551)) ### Changed * Print an error message and return with non-zero exit code when no files are found that match with the globs ([#629](https://github.com/pinterest/ktlint/issue/629)). * Invoke callback on `format` function for all errors including errors that are autocorrected ([#1491](https://github.com/pinterest/ktlint/issues/1491)) * Rename `.editorconfig` property `disabled_rules` to `ktlint_disabled_rules` ([#701](https://github.com/pinterest/ktlint/issues/701)) +* Allow file and directory paths in CLI-parameter "--editorconfig" ([#xxx](https://github.com/pinterest/ktlint/issues/xxx)) ### Removed * Remove support to generate IntelliJ IDEA configuration files as this no longer fits the scope of the ktlint project ([#701](https://github.com/pinterest/ktlint/issues/701)) diff --git a/build.gradle b/build.gradle index c5b5a0eb33..93d3db690e 100644 --- a/build.gradle +++ b/build.gradle @@ -31,11 +31,14 @@ task ktlint(type: JavaExec, group: LifecycleBasePlugin.VERIFICATION_GROUP) { description = "Check Kotlin code style including experimental rules." classpath = configurations.ktlint mainClass.set("com.pinterest.ktlint.Main") - // Experimental rules run by default run on the ktlint code base itself. Experimental rules should not be released if - // we are not pleased ourselves with the results on the ktlint code base. - // Sources in "ktlint/src/test/resources" are excluded as those source contain lint errors that have to be detected by - // unit tests and should not be reported/fixed. - args '**/src/**/*.kt', '!ktlint/src/test/resources/**', '--baseline=ktlint/src/test/resources/test-baseline.xml', '--experimental', '--verbose' + args '**/src/**/*.kt', + // Exclude sources which contain lint violations for the purpose of testing. + '!ktlint/src/test/resources/**', + '--baseline=ktlint/src/test/resources/test-baseline.xml', + // Experimental rules run by default run on the ktlint code base itself. Experimental rules should not be released if + // we are not pleased ourselves with the results on the ktlint code base. + '--experimental', + '--verbose' } // Deployment tasks 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 e75438b358..746349931f 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 @@ -2,6 +2,8 @@ package com.pinterest.ktlint.core import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.codeStyleSetProperty +import com.pinterest.ktlint.core.api.EditorConfigDefaults +import com.pinterest.ktlint.core.api.EditorConfigDefaults.Companion.emptyEditorConfigDefaults import com.pinterest.ktlint.core.api.EditorConfigOverride import com.pinterest.ktlint.core.api.EditorConfigOverride.Companion.emptyEditorConfigOverride import com.pinterest.ktlint.core.api.EditorConfigProperties @@ -10,6 +12,7 @@ import com.pinterest.ktlint.core.internal.EditorConfigGenerator import com.pinterest.ktlint.core.internal.EditorConfigLoader import com.pinterest.ktlint.core.internal.PreparedCode import com.pinterest.ktlint.core.internal.SuppressionLocatorBuilder +import com.pinterest.ktlint.core.internal.ThreadSafeEditorConfigCache.Companion.threadSafeEditorConfigCache import com.pinterest.ktlint.core.internal.VisitorProvider import com.pinterest.ktlint.core.internal.prepareCodeForLinting import com.pinterest.ktlint.core.internal.toQualifiedRuleId @@ -45,9 +48,14 @@ public object KtLint { * [userData] Map of user options. This field is deprecated and will be removed in a future version. * [cb] callback invoked for each lint error * [script] true if this is a Kotlin script file - * [editorConfigPath] optional path of the .editorconfig file (otherwise will use working directory) + * [editorConfigPath] optional path of the .editorconfig file (otherwise will use working directory). Marked for + * removal in KtLint 0.48. Use [editorConfigDefaults] instead * [debug] True if invoked with the --debug flag - * [editorConfigOverride] should contain entries to add/replace from loaded `.editorconfig` files. + * [editorConfigDefaults] contains default values for `.editorconfig` properties which are not set explicitly in + * any '.editorconfig' file located on the path of the [fileName]. If a property is set in [editorConfigDefaults] + * this takes precedence above the default values defined in the KtLint project. + * [editorConfigOverride] should contain entries to add/replace from loaded `.editorconfig` files. If a property is + * set in [editorConfigOverride] it takes precedence above the same property being set in any other way. * * For possible keys check related [Rule]s that implements [UsesEditorConfigProperties] interface. * @@ -70,8 +78,10 @@ public object KtLint { val userData: Map = emptyMap(), // TODO: remove in a future version val cb: (e: LintError, corrected: Boolean) -> Unit, val script: Boolean = false, + @Deprecated("Marked for removal in KtLint 0.48. Use 'editorConfigDefaults' to specify default property values") val editorConfigPath: String? = null, val debug: Boolean = false, + val editorConfigDefaults: EditorConfigDefaults = emptyEditorConfigDefaults, val editorConfigOverride: EditorConfigOverride = emptyEditorConfigOverride, val isInvokedFromCli: Boolean = false ) { @@ -302,10 +312,10 @@ public object KtLint { visitorModifiers.contains(Rule.VisitorModifier.RunOnRootNodeOnly) /** - * Reduce memory usage of all internal caches. + * Reduce memory usage by cleaning internal caches. */ public fun trimMemory() { - editorConfigLoader.trimMemory() + threadSafeEditorConfigCache.clear() } /** diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/api/EditorConfigDefaults.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/api/EditorConfigDefaults.kt new file mode 100644 index 0000000000..bf79cebf60 --- /dev/null +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/api/EditorConfigDefaults.kt @@ -0,0 +1,36 @@ +package com.pinterest.ktlint.core.api + +import com.pinterest.ktlint.core.internal.EditorConfigDefaultsLoader +import java.nio.file.Path +import org.ec4j.core.model.EditorConfig + +/** + * Wrapper around the [EditorConfig]. Only to be used only for the default value of properties. + */ +public data class EditorConfigDefaults(public val value: EditorConfig) { + public companion object { + private val editorConfigDefaultsLoader = EditorConfigDefaultsLoader() + + /** + * Loads properties from [path]. [path] may either locate a file (also allows specifying a file with a name other + * than ".editorconfig") or a directory in which a file with name ".editorconfig" is expected to exist. Properties + * from all globs are returned. + * + * If [path] is not valid then the [emptyEditorConfigDefaults] is returned. + * + * The property "root" which denotes whether the parent directory is to be checked for the existence of a fallback + * ".editorconfig" is ignored entirely. + */ + public fun load(path: Path?): EditorConfigDefaults = + if (path == null) { + emptyEditorConfigDefaults + } else { + editorConfigDefaultsLoader.load(path) + } + + /** + * Empty representation of [EditorConfigDefaults]. + */ + public val emptyEditorConfigDefaults: EditorConfigDefaults = EditorConfigDefaults(EditorConfig.builder().build()) + } +} diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/api/UsesEditorConfigProperties.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/api/UsesEditorConfigProperties.kt index f32ff7a891..e4cfa876b2 100644 --- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/api/UsesEditorConfigProperties.kt +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/api/UsesEditorConfigProperties.kt @@ -91,26 +91,22 @@ public interface UsesEditorConfigProperties { val property = get(editorConfigProperty.type.name) - // If the property value is remapped to a non-null value then return it immediately. - editorConfigProperty - .propertyMapper - ?.invoke(property, codeStyleValue) - ?.let { newValue -> - when { - property == null -> - logger.trace { - "No value of '.editorconfig' property '${editorConfigProperty.type.name}' was found. " + - "Value has been defaulted to '$newValue'. Setting the value explicitly in '.editorconfig' " + - "remove this message from the log." - } - newValue != property.getValueAs() -> + if (property != null) { + editorConfigProperty + .propertyMapper + ?.invoke(property, codeStyleValue) + ?.let { newValue -> + // If the property value is remapped to a non-null value then return it immediately. + val originalValue = property.sourceValue + if (newValue.toString() != originalValue) { logger.trace { - "Value of '.editorconfig' property '${editorConfigProperty.type.name}' is overridden " + - "from '${property.sourceValue}' to '$newValue'" + "Value of '.editorconfig' property '${editorConfigProperty.type.name}' is remapped " + + "from '$originalValue' to '$newValue'" } + } + return newValue } - return newValue - } + } return property?.getValueAs() ?: editorConfigProperty @@ -119,7 +115,7 @@ public interface UsesEditorConfigProperties { logger.trace { "No value of '.editorconfig' property '${editorConfigProperty.type.name}' was found. Value " + "has been defaulted to '$it'. Setting the value explicitly in '.editorconfig' " + - "remove this message from the log." + "removes this message from the log." } } } @@ -297,6 +293,7 @@ public object DefaultEditorConfigProperties : UsesEditorConfigProperties { UsesEditorConfigProperties.EditorConfigProperty( type = PropertyType.max_line_length, defaultValue = -1, + defaultAndroidValue = 100, propertyMapper = { property, codeStyleValue -> when { property == null || property.isUnset -> { @@ -308,7 +305,7 @@ public object DefaultEditorConfigProperties : UsesEditorConfigProperties { } } property.sourceValue == "off" -> -1 - else -> property.getValueAs() + else -> PropertyType.max_line_length.parse(property.sourceValue).parsed } } ) diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigDefaultsLoader.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigDefaultsLoader.kt new file mode 100644 index 0000000000..242d5a2448 --- /dev/null +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigDefaultsLoader.kt @@ -0,0 +1,77 @@ +package com.pinterest.ktlint.core.internal + +import com.pinterest.ktlint.core.api.EditorConfigDefaults +import com.pinterest.ktlint.core.api.EditorConfigDefaults.Companion.emptyEditorConfigDefaults +import com.pinterest.ktlint.core.initKtLintKLogger +import com.pinterest.ktlint.core.internal.ThreadSafeEditorConfigCache.Companion.threadSafeEditorConfigCache +import java.nio.charset.StandardCharsets +import java.nio.file.FileSystem +import java.nio.file.FileSystems +import java.nio.file.Path +import kotlin.io.path.isDirectory +import kotlin.io.path.notExists +import kotlin.io.path.pathString +import mu.KotlinLogging +import org.ec4j.core.EditorConfigLoader +import org.ec4j.core.Resource +import org.ec4j.core.model.Version + +private val logger = KotlinLogging.logger {}.initKtLintKLogger() + +/** + * Load all properties from an ".editorconfig" file without filtering on a glob. + */ +internal class EditorConfigDefaultsLoader( + private val fileSystem: FileSystem = FileSystems.getDefault() +) { + private val editorConfigLoader: EditorConfigLoader = EditorConfigLoader.of(Version.CURRENT) + + /** + * Loads properties from [path]. [path] may either locate a file (also allows specifying a file with a name other + * than ".editorconfig") or a directory in which a file with name ".editorconfig" is expected to exist. Properties + * from all globs are returned. + * + * If [path] is not valid then the [emptyEditorConfigDefaults] is returned. + * + * The property "root" which denotes whether the parent directory is to be checked for the existence of a fallback + * ".editorconfig" is ignored entirely. + */ + fun load(path: Path?): EditorConfigDefaults { + if (path == null || path.pathString.isBlank()) { + return emptyEditorConfigDefaults + } + + val editorConfigFilePath = path.editorConfigFilePath() + if (editorConfigFilePath.notExists()) { + logger.warn { "File or directory '$path' is not found. Can not load '.editorconfig' properties" } + return emptyEditorConfigDefaults + } + + return threadSafeEditorConfigCache + .get(editorConfigFilePath.resource(), editorConfigLoader) + .also { + logger.trace { + it + .toString() + .split("\n") + .joinToString( + prefix = "Loaded .editorconfig-properties from file '$editorConfigFilePath':\n\t", + separator = "\n\t" + ) + } + }.let { EditorConfigDefaults(it) } + } + + private fun Path.editorConfigFilePath() = + if (isDirectory()) { + pathString + .plus( + fileSystem.separator.plus(".editorconfig") + ).let { path -> fileSystem.getPath(path) } + } else { + this + } + + private fun Path.resource() = + Resource.Resources.ofPath(this, StandardCharsets.UTF_8) +} diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoader.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoader.kt index 0fa1c00355..9223c4f6ed 100644 --- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoader.kt +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoader.kt @@ -1,13 +1,17 @@ package com.pinterest.ktlint.core.internal import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.api.EditorConfigDefaults +import com.pinterest.ktlint.core.api.EditorConfigDefaults.Companion.emptyEditorConfigDefaults import com.pinterest.ktlint.core.api.EditorConfigOverride import com.pinterest.ktlint.core.api.EditorConfigOverride.Companion.emptyEditorConfigOverride import com.pinterest.ktlint.core.api.EditorConfigProperties import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import com.pinterest.ktlint.core.initKtLintKLogger +import com.pinterest.ktlint.core.internal.ThreadSafeEditorConfigCache.Companion.threadSafeEditorConfigCache import java.nio.charset.StandardCharsets import java.nio.file.FileSystem +import java.nio.file.FileSystems import java.nio.file.Path import mu.KotlinLogging import org.ec4j.core.EditorConfigLoader @@ -15,27 +19,28 @@ import org.ec4j.core.PropertyTypeRegistry import org.ec4j.core.Resource import org.ec4j.core.ResourcePropertiesService import org.ec4j.core.model.Property +import org.ec4j.core.model.PropertyType import org.ec4j.core.model.Version +import org.jetbrains.kotlin.utils.addToStdlib.applyIf private val logger = KotlinLogging.logger {}.initKtLintKLogger() /** - * Loads default `.editorconfig` and ktlint specific properties for files. - * - * Contains internal in-memory cache to speedup lookup. + * Loader for `.editorconfig` properties for files on [fileSystem]. */ -public class EditorConfigLoader( - private val fs: FileSystem -) { - private val cache = ThreadSafeEditorConfigCache() - +public class EditorConfigLoader(private val fileSystem: FileSystem = FileSystems.getDefault()) { /** + * DEPRECATION NOTICE: + * This method is removed from the public API in KtLint 0.48.0. Please raise an issue if you have a use case as + * consumer of this API. If you currently rely on this method, then migrate to the replacement method which offers + * a more consistent interface and allows to use both defaults and overrides of the '.editorconfig' files which are + * found on the [filePath]. + * * Loads applicable properties from `.editorconfig`s for given file. * * @param filePath path to file that would be checked. - * @param isStdIn indicates that checked content comes from input. - * Setting this to `true` overrides [filePath] and uses `.kt` pattern to load properties - * from current folder `.editorconfig` files. + * @param isStdIn indicates that checked content comes from input. Setting this to `true` overrides [filePath] and + * uses `.kt` pattern to load properties from current folder `.editorconfig` files. * @param alternativeEditorConfig alternative to current [filePath] location where `.editorconfig` files should be * looked up * @param rules set of [Rule]s linting the file @@ -43,9 +48,13 @@ public class EditorConfigLoader( * @param debug pass `true` to enable some additional debug output * * @return all possible loaded properties applicable to given file. - * In case file extensions is not one of [SUPPORTED_FILES] or [filePath] is `null` - * method will immediately return empty map. + * In case file extensions is not one of [SUPPORTED_FILES] or [filePath] is `null` method will immediately return + * empty map. */ + @Deprecated( + message = "Marked for removal in ktlint 0.48.0. See kdoc or changelog for more information", + replaceWith = ReplaceWith("loadForFile(filePath)") + ) public fun loadPropertiesForFile( filePath: Path?, isStdIn: Boolean = false, @@ -54,92 +63,180 @@ public class EditorConfigLoader( editorConfigOverride: EditorConfigOverride = emptyEditorConfigOverride, debug: Boolean = false ): EditorConfigProperties { - if (!isStdIn && - (filePath == null || SUPPORTED_FILES.none { filePath.toString().endsWith(it) }) - ) { + if (!isStdIn && filePath.isNullOrNotSupported()) { return editorConfigOverride .properties - .map { (property, value) -> - property.type.name to Property.builder() - .name(property.type.name) - .type(property.type) - .value(value) - .build() + .map { (editorConfigProperty, propertyValue) -> + editorConfigProperty.type.name to property(editorConfigProperty, propertyValue) }.toMap() } - val propService = createLoaderService(rules) - val normalizedFilePath = when { alternativeEditorConfig != null -> { - val editorconfigFilePath = if (isStdIn) "stdin${SUPPORTED_FILES.first()}" else filePath!!.last() + val editorconfigFilePath = + if (isStdIn) { + "stdin${SUPPORTED_FILES.first()}" + } else { + filePath!!.last() + } alternativeEditorConfig .toAbsolutePath() .resolve("$editorconfigFilePath") } - isStdIn -> - fs - .getPath(".") - .toAbsolutePath() - .resolve("stdin${SUPPORTED_FILES.first()}") + isStdIn -> defaultFilePath() else -> filePath } - return propService - .queryProperties( - Resource.Resources.ofPath(normalizedFilePath, StandardCharsets.UTF_8) - ) + return createLoaderService(rules, emptyEditorConfigDefaults) + .queryProperties(normalizedFilePath.resource()) .properties .also { loaded -> editorConfigOverride .properties .forEach { - loaded[it.key.type.name] = Property.builder() - .name(it.key.type.name) - .type(it.key.type) - .value(it.value) - .build() + loaded[it.key.type.name] = property(it.key, it.value) } - } - .also { - logger.trace { - it - .map { entry -> "${entry.key}: ${entry.value.sourceValue}" } - .joinToString( - prefix = "Resolving .editorconfig files for $normalizedFilePath file path:\n\t", - separator = ", " - ) - } + }.also { editorConfigProperties -> + logger.trace { editorConfigProperties.prettyPrint(normalizedFilePath) } } } /** - * Trims used in-memory cache. + * DEPRECATION NOTICE: + * This method is removed from the public API in KtLint 0.48.0. Please raise an issue if you have a use case as + * consumer of this API. In case you migrate from the old method to this method, then just let us know in which + * case this method will be kept available in the public API. + * + * Loads properties used by [Rule]s from the `.editorconfig` file on given [filePath]. When [filePath] is null, the + * properties for the ".kt" pattern in the current directory are loaded. The '.editorconfig' files on the [filePath] + * are read starting from the [filePath] upwards until an '.editorconfig' file is found in which the property "root" + * is found with value "true" or until the root of the filesystem is reached. + * + * Properties specified in [editorConfigDefaults] will be used in case the property was not found in any + * '.editorconfig' on [filePath]. If the property is not specified in [editorConfigDefaults] then the default value + * as specified in the property definition [UsesEditorConfigProperties.EditorConfigProperty] is used. + * + * Properties specified in [editorConfigOverride] take precedence above any other '.editorconfig' file on [filePath] + * or default value. */ - public fun trimMemory() { - cache.clear() + @Deprecated("Marked for removal from the public API in KtLint 0.48. See KDoc or changelog for more information") + public fun load( + filePath: Path?, + rules: Set = emptySet(), + editorConfigDefaults: EditorConfigDefaults = emptyEditorConfigDefaults, + editorConfigOverride: EditorConfigOverride = emptyEditorConfigOverride + ): EditorConfigProperties { + if (filePath.isNullOrNotSupported()) { + return editorConfigOverride + .properties + .map { (editorConfigProperty, propertyValue) -> + editorConfigProperty.type.name to property(editorConfigProperty, propertyValue) + }.toMap() + } + + // TODO: Move to class init once method load PropertiesForFiles has been removed. + require(rules.isNotEmpty()) { + "Set of rules for which the properties have to be loaded may not be empty." + } + + val normalizedFilePath = filePath ?: defaultFilePath() + + return createLoaderService(rules, editorConfigDefaults) + .queryProperties(normalizedFilePath.resource()) + .properties + .also { loaded -> + editorConfigOverride + .properties + .forEach { + loaded[it.key.type.name] = property(it.key, it.value) + } + }.also { editorConfigProperties -> + logger.trace { editorConfigProperties.prettyPrint(normalizedFilePath) } + } } + private fun MutableMap.prettyPrint( + normalizedFilePath: Path? + ) = map { entry -> "${entry.key}: ${entry.value.sourceValue}" } + .joinToString( + prefix = "Resolving .editorconfig files for $normalizedFilePath file path:\n\t", + separator = "\n\t" + ) + + private fun Path?.resource() = + Resource.Resources.ofPath(this, StandardCharsets.UTF_8) + + private fun property( + property: UsesEditorConfigProperties.EditorConfigProperty<*>, + value: PropertyType.PropertyValue<*> + ) = Property + .builder() + .name(property.type.name) + .type(property.type) + .value(value) + .build() + + private fun defaultFilePath() = + fileSystem + .getPath(".") + .toAbsolutePath() + .resolve(SUPPORTED_FILES.first()) + + private fun Path?.isNullOrNotSupported() = + this == null || this.isNotSupported() + + private fun Path.isNotSupported() = + SUPPORTED_FILES + .none { + this.toString().endsWith(it) + } + private fun createLoaderService( - rules: Set - ): ResourcePropertiesService { - val propertyTypeRegistry = PropertyTypeRegistry.builder() + rules: Set, + editorConfigDefaults: EditorConfigDefaults + ) = createResourcePropertiesService( + editorConfigLoader(rules), + editorConfigDefaults + ) + + private fun createResourcePropertiesService( + editorConfigLoader: EditorConfigLoader, + editorConfigDefaults: EditorConfigDefaults + ) = + ResourcePropertiesService.builder() + .keepUnset(true) + .cache(threadSafeEditorConfigCache) + .loader(editorConfigLoader) + .applyIf(editorConfigDefaults != emptyEditorConfigDefaults) { + defaultEditorConfigs(editorConfigDefaults.value) + }.build() + + private fun editorConfigLoader(rules: Set) = + EditorConfigLoader + .of(Version.CURRENT, propertyTypeRegistry(rules)) + + private fun propertyTypeRegistry(rules: Set) = + PropertyTypeRegistry.builder() .defaults() .apply { rules .filterIsInstance() .flatMap(UsesEditorConfigProperties::editorConfigProperties) - .forEach { prop -> - type(prop.type) + .forEach { editorConfigProperty -> + type(editorConfigProperty.type) } } .build() - val editorConfigLoader = EditorConfigLoader.of(Version.CURRENT, propertyTypeRegistry) - return ResourcePropertiesService.builder() - .keepUnset(true) - .cache(cache) - .loader(editorConfigLoader) - .build() + + /** + * Trims used in-memory cache. + */ + @Deprecated( + message = "Marked for removal in KtLint 0.48.0", + replaceWith = ReplaceWith("KtLint.trimMemory()") + ) + public fun trimMemory() { + threadSafeEditorConfigCache.clear() } public companion object { @@ -156,6 +253,7 @@ public class EditorConfigLoader( * * @return map of key as string and value as string property representation */ + @Deprecated(message = "Marked for removal of public API in KtLint 0.48") public fun EditorConfigProperties.convertToRawValues(): Map { return if (isEmpty()) { emptyMap() diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/PreparedCode.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/PreparedCode.kt index 0312c391f6..ca81c210b9 100644 --- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/PreparedCode.kt +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/PreparedCode.kt @@ -3,7 +3,6 @@ package com.pinterest.ktlint.core.internal import com.pinterest.ktlint.core.KtLint import com.pinterest.ktlint.core.ParseException import com.pinterest.ktlint.core.api.EditorConfigProperties -import java.nio.file.Paths import org.jetbrains.kotlin.com.intellij.lang.FileASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.com.intellij.psi.PsiErrorElement @@ -43,13 +42,11 @@ internal fun prepareCodeForLinting(params: KtLint.ExperimentalParams): PreparedC val rootNode = psiFile.node - val editorConfigProperties = KtLint.editorConfigLoader.loadPropertiesForFile( - params.normalizedFilePath, - params.isStdIn, - params.editorConfigPath?.let { Paths.get(it) }, - params.getRules(), - params.editorConfigOverride, - params.debug + val editorConfigProperties = KtLint.editorConfigLoader.load( + filePath = params.normalizedFilePath, + rules = params.getRules(), + editorConfigDefaults = params.editorConfigDefaults, + editorConfigOverride = params.editorConfigOverride ) if (!params.isStdIn) { diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/ThreadSafeEditorConfigCache.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/ThreadSafeEditorConfigCache.kt index 2f277f28d3..8c29de0e8c 100644 --- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/ThreadSafeEditorConfigCache.kt +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/internal/ThreadSafeEditorConfigCache.kt @@ -15,14 +15,18 @@ internal class ThreadSafeEditorConfigCache : Cache { private val readWriteLock = ReentrantReadWriteLock() private val inMemoryMap = HashMap() + /** + * Gets the [editorConfigFile] from the cache. If not found, then the [editorConfigLoader] is used to retrieve the + * '.editorconfig' properties. The result is stored in the cache. + */ override fun get( editorConfigFile: Resource, - loader: EditorConfigLoader + editorConfigLoader: EditorConfigLoader ): EditorConfig { readWriteLock.read { return inMemoryMap[editorConfigFile] ?: readWriteLock.write { - val result = loader.load(editorConfigFile) + val result = editorConfigLoader.load(editorConfigFile) inMemoryMap[editorConfigFile] = result result } @@ -32,4 +36,8 @@ internal class ThreadSafeEditorConfigCache : Cache { fun clear() = readWriteLock.write { inMemoryMap.clear() } + + internal companion object { + val threadSafeEditorConfigCache = ThreadSafeEditorConfigCache() + } } diff --git a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigDefaultsLoaderTest.kt b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigDefaultsLoaderTest.kt new file mode 100644 index 0000000000..53d09a72b3 --- /dev/null +++ b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigDefaultsLoaderTest.kt @@ -0,0 +1,138 @@ +package com.pinterest.ktlint.core.internal + +import com.google.common.jimfs.Configuration +import com.google.common.jimfs.Jimfs +import com.pinterest.ktlint.core.api.EditorConfigDefaults +import com.pinterest.ktlint.core.api.EditorConfigDefaults.Companion.emptyEditorConfigDefaults +import java.nio.file.FileSystem +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.isDirectory +import org.assertj.core.api.Assertions.assertThat +import org.ec4j.core.model.EditorConfig +import org.ec4j.core.model.Glob +import org.ec4j.core.model.Property +import org.ec4j.core.model.Section +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class EditorConfigDefaultsLoaderTest { + private val fileSystemMock = Jimfs.newFileSystem(Configuration.forCurrentPlatform()) + private val editorConfigDefaultsLoader = EditorConfigDefaultsLoader(fileSystemMock) + + @AfterEach + internal fun tearDown() { + fileSystemMock.close() + } + + @Test + fun `Given a null path then return empty editor config default`() { + val actual = editorConfigDefaultsLoader.load(null) + + assertThat(actual).isEqualTo(emptyEditorConfigDefaults) + } + + @Test + fun `Given an empty path then return empty editor config default`() { + val actual = editorConfigDefaultsLoader.load( + fileSystemMock.normalizedPath("") + ) + + assertThat(actual).isEqualTo(emptyEditorConfigDefaults) + } + + @Test + fun `Given a blank path then return empty editor config default`() { + val actual = editorConfigDefaultsLoader.load( + fileSystemMock.normalizedPath(" ") + ) + + assertThat(actual).isEqualTo(emptyEditorConfigDefaults) + } + + @Test + fun `Given an non existing path then return empty editor config default`() { + val actual = editorConfigDefaultsLoader.load( + fileSystemMock.normalizedPath("/path/to/non/existing/file.kt") + ) + + assertThat(actual).isEqualTo(emptyEditorConfigDefaults) + } + + @ParameterizedTest(name = "Filename: {0}") + @ValueSource( + strings = [ + ".editorconfig", + "some-alternative-file-name" + ] + ) + fun `Given an existing editor config file then load all settings from it`( + fileName: String + ) { + val existingEditorConfigFileName = "/some/path/to/existing/$fileName" + fileSystemMock.writeEditorConfigFile( + existingEditorConfigFileName, + SOME_EDITOR_CONFIG.toString() + ) + + val actual = editorConfigDefaultsLoader.load( + fileSystemMock.normalizedPath(existingEditorConfigFileName) + ) + + assertThat(actual).isEqualTo( + EditorConfigDefaults(SOME_EDITOR_CONFIG) + ) + } + + @Test + fun `Given an existing directory containing an editor config file then load all settings from it`() { + val existingDirectory = "/some/path/to/existing/directory" + fileSystemMock.writeEditorConfigFile( + existingDirectory.plus("/.editorconfig"), + SOME_EDITOR_CONFIG.toString() + ) + + val actual = editorConfigDefaultsLoader.load( + fileSystemMock.normalizedPath(existingDirectory) + ) + + assertThat(actual).isEqualTo( + EditorConfigDefaults(SOME_EDITOR_CONFIG) + ) + } + + private fun FileSystem.writeEditorConfigFile( + filePath: String, + content: String + ) { + val path = normalizedPath(filePath) + require(!path.isDirectory()) + + Files.createDirectories(path.parent) + Files.write(path, content.toByteArray()) + } + + private fun FileSystem.normalizedPath(path: String): Path { + val root = rootDirectories.joinToString(separator = "/") + return getPath("$root$path") + } + + private companion object { + val SOME_EDITOR_CONFIG: EditorConfig = EditorConfig + .builder() + .section( + Section + .builder() + .glob(Glob("*.kt")) + .properties( + Property + .builder() + .name("some-property") + .value("some-property-value") + ) + ) + .build() + } +} diff --git a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoaderTest.kt b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoaderTest.kt index b9ac707d2c..d82b3606d2 100644 --- a/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoaderTest.kt +++ b/ktlint-core/src/test/kotlin/com/pinterest/ktlint/core/internal/EditorConfigLoaderTest.kt @@ -16,36 +16,24 @@ import org.assertj.core.api.Assertions.entry import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test internal class EditorConfigLoaderTest { - private val tempFileSystem = Jimfs.newFileSystem(Configuration.forCurrentPlatform()) - private val editorConfigLoader = EditorConfigLoader(tempFileSystem) + private val fileSystemMock = Jimfs.newFileSystem(Configuration.forCurrentPlatform()) + private val editorConfigLoader = EditorConfigLoader(fileSystemMock) private val rules = setOf(TestRule()) - private fun FileSystem.normalizedPath(path: String): Path { - val root = rootDirectories.joinToString(separator = "/") - return getPath("$root$path") - } - - private fun FileSystem.writeEditorConfigFile( - filePath: String, - content: String - ) { - Files.createDirectories(normalizedPath(filePath)) - Files.write(normalizedPath("$filePath/.editorconfig"), content.toByteArray()) - } - @AfterEach fun tearDown() { - tempFileSystem.close() + fileSystemMock.close() } @Test fun testParentDirectoryFallback() { val projectDir = "/projects/project-1" val projectSubDirectory = "$projectDir/project-1-subdirectory" - Files.createDirectories(tempFileSystem.normalizedPath(projectSubDirectory)) + Files.createDirectories(fileSystemMock.normalizedPath(projectSubDirectory)) //language=EditorConfig val editorConfigFiles = arrayOf( """ @@ -72,10 +60,10 @@ internal class EditorConfigLoaderTest { ) editorConfigFiles.forEach { editorConfigFileContent -> - tempFileSystem.writeEditorConfigFile(projectDir, editorConfigFileContent) + fileSystemMock.writeEditorConfigFile(projectDir, editorConfigFileContent) - val lintFile = tempFileSystem.normalizedPath(projectDir).resolve("test.kt") - val editorConfig = editorConfigLoader.loadPropertiesForFile(lintFile, rules = rules) + val lintFile = fileSystemMock.normalizedPath(projectDir).resolve("test.kt") + val editorConfig = editorConfigLoader.load(lintFile, rules = rules) val parsedEditorConfig = editorConfig.convertToRawValues() assertThat(parsedEditorConfig).isNotEmpty @@ -93,6 +81,7 @@ internal class EditorConfigLoaderTest { } } + //language= @Test fun testRootTermination() { val rootDir = "/projects" @@ -100,7 +89,7 @@ internal class EditorConfigLoaderTest { val project1Subdirectory = "$project1Dir/project-1-subdirectory" //language=EditorConfig - tempFileSystem.writeEditorConfigFile( + fileSystemMock.writeEditorConfigFile( rootDir, """ root = true @@ -110,7 +99,7 @@ internal class EditorConfigLoaderTest { ) //language=EditorConfig - tempFileSystem.writeEditorConfigFile( + fileSystemMock.writeEditorConfigFile( project1Dir, """ root = true @@ -121,7 +110,7 @@ internal class EditorConfigLoaderTest { ) //language=EditorConfig - tempFileSystem.writeEditorConfigFile( + fileSystemMock.writeEditorConfigFile( project1Subdirectory, """ [*] @@ -129,8 +118,8 @@ internal class EditorConfigLoaderTest { """.trimIndent() ) - val lintFileSubdirectory = tempFileSystem.normalizedPath(project1Subdirectory).resolve("test.kt") - var editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFileSubdirectory, rules = rules) + val lintFileSubdirectory = fileSystemMock.normalizedPath(project1Subdirectory).resolve("test.kt") + var editorConfigProperties = editorConfigLoader.load(lintFileSubdirectory, rules = rules) var parsedEditorConfig = editorConfigProperties.convertToRawValues() assertThat(parsedEditorConfig).isEqualTo( @@ -141,8 +130,8 @@ internal class EditorConfigLoaderTest { ) ) - val lintFileMainDir = tempFileSystem.normalizedPath(project1Dir).resolve("test.kts") - editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFileMainDir, rules = rules) + val lintFileMainDir = fileSystemMock.normalizedPath(project1Dir).resolve("test.kts") + editorConfigProperties = editorConfigLoader.load(lintFileMainDir, rules = rules) parsedEditorConfig = editorConfigProperties.convertToRawValues() assertThat(parsedEditorConfig).isEqualTo( @@ -153,8 +142,8 @@ internal class EditorConfigLoaderTest { ) ) - val lintFileRoot = tempFileSystem.normalizedPath(rootDir).resolve("test.kt") - editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFileRoot, rules = rules) + val lintFileRoot = fileSystemMock.normalizedPath(rootDir).resolve("test.kt") + editorConfigProperties = editorConfigLoader.load(lintFileRoot, rules = rules) parsedEditorConfig = editorConfigProperties.convertToRawValues() assertThat(parsedEditorConfig).isEqualTo( @@ -175,10 +164,10 @@ internal class EditorConfigLoaderTest { insert_final_newline = true disabled_rules = import-ordering """.trimIndent() - tempFileSystem.writeEditorConfigFile(projectDir, editorconfigFile) + fileSystemMock.writeEditorConfigFile(projectDir, editorconfigFile) - val lintFile = tempFileSystem.normalizedPath(projectDir).resolve("test.kt") - val editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFile, rules = rules) + val lintFile = fileSystemMock.normalizedPath(projectDir).resolve("test.kt") + val editorConfigProperties = editorConfigLoader.load(lintFile, rules = rules) val parsedEditorConfig = editorConfigProperties.convertToRawValues() assertThat(parsedEditorConfig).isNotEmpty @@ -200,10 +189,10 @@ internal class EditorConfigLoaderTest { [*.{kt,kts}] indent_size = unset """.trimIndent() - tempFileSystem.writeEditorConfigFile(projectDir, editorconfigFile) + fileSystemMock.writeEditorConfigFile(projectDir, editorconfigFile) - val lintFile = tempFileSystem.normalizedPath(projectDir).resolve("test.kt") - val editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFile, rules = rules) + val lintFile = fileSystemMock.normalizedPath(projectDir).resolve("test.kt") + val editorConfigProperties = editorConfigLoader.load(lintFile, rules = rules) val parsedEditorConfig = editorConfigProperties.convertToRawValues() assertThat(parsedEditorConfig).isNotEmpty @@ -225,10 +214,10 @@ internal class EditorConfigLoaderTest { [*.{kt,kts}] disabled_rules=import-ordering, no-wildcard-imports """.trimIndent() - tempFileSystem.writeEditorConfigFile(projectDir, editorconfigFile) - val lintFile = tempFileSystem.normalizedPath(projectDir).resolve("test.kts") + fileSystemMock.writeEditorConfigFile(projectDir, editorconfigFile) + val lintFile = fileSystemMock.normalizedPath(projectDir).resolve("test.kts") - val editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFile, rules = rules) + val editorConfigProperties = editorConfigLoader.load(lintFile, rules = rules) val parsedEditorConfig = editorConfigProperties.convertToRawValues() assertThat(parsedEditorConfig).isNotEmpty @@ -241,7 +230,7 @@ internal class EditorConfigLoaderTest { @Test fun `Should return the override properties only on null file path`() { - val parsedEditorConfig = editorConfigLoader.loadPropertiesForFile( + val parsedEditorConfig = editorConfigLoader.load( filePath = null, rules = rules, editorConfigOverride = EditorConfigOverride.from(insertNewLineProperty to "true") @@ -254,7 +243,7 @@ internal class EditorConfigLoaderTest { @Test fun `Should return the override properties only non supported file`() { - val parsedEditorConfig = editorConfigLoader.loadPropertiesForFile( + val parsedEditorConfig = editorConfigLoader.load( filePath = null, rules = rules, editorConfigOverride = EditorConfigOverride.from(insertNewLineProperty to "true") @@ -274,7 +263,7 @@ internal class EditorConfigLoaderTest { insert_final_newline = true disabled_rules = import-ordering """.trimIndent() - tempFileSystem.writeEditorConfigFile(".", editorconfigFile) + fileSystemMock.writeEditorConfigFile(".", editorconfigFile) val editorConfigProperties = editorConfigLoader.loadPropertiesForFile( filePath = null, @@ -293,6 +282,7 @@ internal class EditorConfigLoaderTest { ) } + //language= @Test fun `Should load properties from alternative provided editorconfig file`() { val rootDir = "/projects" @@ -300,7 +290,7 @@ internal class EditorConfigLoaderTest { val anotherDir = "$rootDir/project-2-dir" //language=EditorConfig - tempFileSystem.writeEditorConfigFile( + fileSystemMock.writeEditorConfigFile( rootDir, """ root = true @@ -310,7 +300,7 @@ internal class EditorConfigLoaderTest { ) //language=EditorConfig - tempFileSystem.writeEditorConfigFile( + fileSystemMock.writeEditorConfigFile( mainProjectDir, """ root = true @@ -321,7 +311,7 @@ internal class EditorConfigLoaderTest { ) //language=EditorConfig - tempFileSystem.writeEditorConfigFile( + fileSystemMock.writeEditorConfigFile( anotherDir, """ [*] @@ -329,10 +319,10 @@ internal class EditorConfigLoaderTest { """.trimIndent() ) - val lintFile = tempFileSystem.normalizedPath(mainProjectDir).resolve("test.kt") + val lintFile = fileSystemMock.normalizedPath(mainProjectDir).resolve("test.kt") val editorConfigProperties = editorConfigLoader.loadPropertiesForFile( filePath = lintFile, - alternativeEditorConfig = tempFileSystem.normalizedPath(anotherDir).resolve(".editorconfig"), + alternativeEditorConfig = fileSystemMock.normalizedPath(anotherDir).resolve(".editorconfig"), rules = rules ) val parsedEditorConfig = editorConfigProperties.convertToRawValues() @@ -347,13 +337,14 @@ internal class EditorConfigLoaderTest { ) } + //language= @Test fun `Should load properties from alternative editorconfig on stdin input`() { val rootDir = "/projects" val anotherDir = "$rootDir/project-2-dir" //language=EditorConfig - tempFileSystem.writeEditorConfigFile( + fileSystemMock.writeEditorConfigFile( rootDir, """ root = true @@ -363,7 +354,7 @@ internal class EditorConfigLoaderTest { ) //language=EditorConfig - tempFileSystem.writeEditorConfigFile( + fileSystemMock.writeEditorConfigFile( anotherDir, """ [*] @@ -373,7 +364,7 @@ internal class EditorConfigLoaderTest { val editorConfigProperties = editorConfigLoader.loadPropertiesForFile( filePath = null, - alternativeEditorConfig = tempFileSystem.normalizedPath(anotherDir).resolve(".editorconfig"), + alternativeEditorConfig = fileSystemMock.normalizedPath(anotherDir).resolve(".editorconfig"), isStdIn = true, rules = rules ) @@ -403,9 +394,9 @@ internal class EditorConfigLoaderTest { [api/*.{kt,kts}] disabled_rules = class-must-be-internal """.trimIndent() - tempFileSystem.writeEditorConfigFile(projectDir, editorconfigFile) + fileSystemMock.writeEditorConfigFile(projectDir, editorconfigFile) - val lintFile = tempFileSystem.normalizedPath(projectDir).resolve("api").resolve("test.kt") + val lintFile = fileSystemMock.normalizedPath(projectDir).resolve("api").resolve("test.kt") Files.createDirectories(lintFile) val editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFile, debug = true, rules = rules) @@ -430,11 +421,11 @@ internal class EditorConfigLoaderTest { [*.{kt,kts}] disabled_rules=import-ordering, no-wildcard-imports """.trimIndent() - tempFileSystem.writeEditorConfigFile(projectDir, editorconfigFile) + fileSystemMock.writeEditorConfigFile(projectDir, editorconfigFile) - val lintFile = tempFileSystem.normalizedPath(projectDir).resolve("test.kts") + val lintFile = fileSystemMock.normalizedPath(projectDir).resolve("test.kts") - val editorConfigProperties = editorConfigLoader.loadPropertiesForFile( + val editorConfigProperties = editorConfigLoader.load( lintFile, rules = rules.plus(FinalNewlineRule()), editorConfigOverride = EditorConfigOverride.from(insertNewLineProperty to "true") @@ -460,11 +451,11 @@ internal class EditorConfigLoaderTest { [*.{kt,kts}] insert_final_newline = true """.trimIndent() - tempFileSystem.writeEditorConfigFile(projectDir, editorconfigFile) + fileSystemMock.writeEditorConfigFile(projectDir, editorconfigFile) - val lintFile = tempFileSystem.normalizedPath(projectDir).resolve("test.kts") + val lintFile = fileSystemMock.normalizedPath(projectDir).resolve("test.kts") - val editorConfigProperties = editorConfigLoader.loadPropertiesForFile( + val editorConfigProperties = editorConfigLoader.load( lintFile, rules = rules.plus(FinalNewlineRule()), editorConfigOverride = EditorConfigOverride.from(insertNewLineProperty to "false") @@ -479,6 +470,464 @@ internal class EditorConfigLoaderTest { ) } + // TODO: To be removed when method loadPropertiesForFile is removed + @Nested + inner class LoadPropertiesForFile { + @Test + fun testParentDirectoryFallback() { + val projectDir = "/projects/project-1" + val projectSubDirectory = "$projectDir/project-1-subdirectory" + Files.createDirectories(fileSystemMock.normalizedPath(projectSubDirectory)) + //language=EditorConfig + 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 -> + fileSystemMock.writeEditorConfigFile(projectDir, editorConfigFileContent) + + val lintFile = fileSystemMock.normalizedPath(projectDir).resolve("test.kt") + val editorConfig = editorConfigLoader.loadPropertiesForFile(lintFile, rules = rules) + val parsedEditorConfig = editorConfig.convertToRawValues() + + assertThat(parsedEditorConfig).isNotEmpty + assertThat(parsedEditorConfig) + .overridingErrorMessage( + "Expected \n%s\nto yield indent_size = 2", + editorConfigFileContent + ) + .isEqualTo( + mapOf( + "indent_size" to "2", + "tab_width" to "2" + ) + ) + } + } + + //language= + @Test + fun testRootTermination() { + val rootDir = "/projects" + val project1Dir = "$rootDir/project-1" + val project1Subdirectory = "$project1Dir/project-1-subdirectory" + + //language=EditorConfig + fileSystemMock.writeEditorConfigFile( + rootDir, + """ + root = true + [*] + end_of_line = lf + """.trimIndent() + ) + + //language=EditorConfig + fileSystemMock.writeEditorConfigFile( + project1Dir, + """ + root = true + [*.{kt,kts}] + indent_size = 4 + indent_style = space + """.trimIndent() + ) + + //language=EditorConfig + fileSystemMock.writeEditorConfigFile( + project1Subdirectory, + """ + [*] + indent_size = 2 + """.trimIndent() + ) + + val lintFileSubdirectory = fileSystemMock.normalizedPath(project1Subdirectory).resolve("test.kt") + var editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFileSubdirectory, rules = rules) + var parsedEditorConfig = editorConfigProperties.convertToRawValues() + + assertThat(parsedEditorConfig).isEqualTo( + mapOf( + "indent_size" to "2", + "tab_width" to "2", + "indent_style" to "space" + ) + ) + + val lintFileMainDir = fileSystemMock.normalizedPath(project1Dir).resolve("test.kts") + editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFileMainDir, rules = rules) + parsedEditorConfig = editorConfigProperties.convertToRawValues() + + assertThat(parsedEditorConfig).isEqualTo( + mapOf( + "indent_size" to "4", + "tab_width" to "4", + "indent_style" to "space" + ) + ) + + val lintFileRoot = fileSystemMock.normalizedPath(rootDir).resolve("test.kt") + editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFileRoot, rules = rules) + parsedEditorConfig = editorConfigProperties.convertToRawValues() + + assertThat(parsedEditorConfig).isEqualTo( + mapOf( + "end_of_line" to "lf" + ) + ) + } + + @Test + fun `Should parse assignment with spaces`() { + val projectDir = "/project" + + @Language("EditorConfig") + val editorconfigFile = + """ + [*.{kt,kts}] + insert_final_newline = true + disabled_rules = import-ordering + """.trimIndent() + fileSystemMock.writeEditorConfigFile(projectDir, editorconfigFile) + + val lintFile = fileSystemMock.normalizedPath(projectDir).resolve("test.kt") + val editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFile, rules = rules) + val parsedEditorConfig = editorConfigProperties.convertToRawValues() + + assertThat(parsedEditorConfig).isNotEmpty + assertThat(parsedEditorConfig).isEqualTo( + mapOf( + "insert_final_newline" to "true", + "disabled_rules" to "import-ordering" + ) + ) + } + + @Test + fun `Should parse unset values`() { + val projectDir = "/project" + + @Language("EditorConfig") + val editorconfigFile = + """ + [*.{kt,kts}] + indent_size = unset + """.trimIndent() + fileSystemMock.writeEditorConfigFile(projectDir, editorconfigFile) + + val lintFile = fileSystemMock.normalizedPath(projectDir).resolve("test.kt") + val editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFile, rules = rules) + val parsedEditorConfig = editorConfigProperties.convertToRawValues() + + assertThat(parsedEditorConfig).isNotEmpty + assertThat(parsedEditorConfig).isEqualTo( + mapOf( + "indent_size" to "unset", + "tab_width" to "unset" + ) + ) + } + + @Test + fun `Should parse list with spaces after comma`() { + val projectDir = "/project" + + @Language("EditorConfig") + val editorconfigFile = + """ + [*.{kt,kts}] + disabled_rules=import-ordering, no-wildcard-imports + """.trimIndent() + fileSystemMock.writeEditorConfigFile(projectDir, editorconfigFile) + val lintFile = fileSystemMock.normalizedPath(projectDir).resolve("test.kts") + + val editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFile, rules = rules) + val parsedEditorConfig = editorConfigProperties.convertToRawValues() + + assertThat(parsedEditorConfig).isNotEmpty + assertThat(parsedEditorConfig).isEqualTo( + mapOf( + "disabled_rules" to "import-ordering, no-wildcard-imports" + ) + ) + } + + @Test + fun `Should return the override properties only on null file path`() { + val parsedEditorConfig = editorConfigLoader.loadPropertiesForFile( + filePath = null, + rules = rules, + editorConfigOverride = EditorConfigOverride.from(insertNewLineProperty to "true") + ) + + assertThat(parsedEditorConfig.convertToRawValues()).containsExactly( + entry("insert_final_newline", "true") + ) + } + + @Test + fun `Should return the override properties only non supported file`() { + val parsedEditorConfig = editorConfigLoader.loadPropertiesForFile( + filePath = null, + rules = rules, + editorConfigOverride = EditorConfigOverride.from(insertNewLineProperty to "true") + ) + + assertThat(parsedEditorConfig.convertToRawValues()).containsExactly( + entry("insert_final_newline", "true") + ) + } + + @Test + fun `Should return properties for stdin from current directory`() { + @Language("EditorConfig") + val editorconfigFile = + """ + [*.{kt,kts}] + insert_final_newline = true + disabled_rules = import-ordering + """.trimIndent() + fileSystemMock.writeEditorConfigFile(".", editorconfigFile) + + val editorConfigProperties = editorConfigLoader.loadPropertiesForFile( + filePath = null, + isStdIn = true, + rules = rules, + debug = true + ) + val parsedEditorConfig = editorConfigProperties.convertToRawValues() + + assertThat(parsedEditorConfig).isNotEmpty + assertThat(parsedEditorConfig).isEqualTo( + mapOf( + "insert_final_newline" to "true", + "disabled_rules" to "import-ordering" + ) + ) + } + + //language= + @Test + fun `Should load properties from alternative provided editorconfig file`() { + val rootDir = "/projects" + val mainProjectDir = "$rootDir/project-1" + val anotherDir = "$rootDir/project-2-dir" + + //language=EditorConfig + fileSystemMock.writeEditorConfigFile( + rootDir, + """ + root = true + [*] + end_of_line = lf + """.trimIndent() + ) + + //language=EditorConfig + fileSystemMock.writeEditorConfigFile( + mainProjectDir, + """ + root = true + [*.{kt,kts}] + indent_size = 4 + indent_style = space + """.trimIndent() + ) + + //language=EditorConfig + fileSystemMock.writeEditorConfigFile( + anotherDir, + """ + [*] + indent_size = 2 + """.trimIndent() + ) + + val lintFile = fileSystemMock.normalizedPath(mainProjectDir).resolve("test.kt") + val editorConfigProperties = editorConfigLoader.loadPropertiesForFile( + filePath = lintFile, + alternativeEditorConfig = fileSystemMock.normalizedPath(anotherDir).resolve(".editorconfig"), + rules = rules + ) + val parsedEditorConfig = editorConfigProperties.convertToRawValues() + + assertThat(parsedEditorConfig).isNotEmpty + assertThat(parsedEditorConfig).isEqualTo( + mapOf( + "end_of_line" to "lf", + "indent_size" to "2", + "tab_width" to "2" + ) + ) + } + + //language= + @Test + fun `Should load properties from alternative editorconfig on stdin input`() { + val rootDir = "/projects" + val anotherDir = "$rootDir/project-2-dir" + + //language=EditorConfig + fileSystemMock.writeEditorConfigFile( + rootDir, + """ + root = true + [*] + end_of_line = lf + """.trimIndent() + ) + + //language=EditorConfig + fileSystemMock.writeEditorConfigFile( + anotherDir, + """ + [*] + indent_size = 2 + """.trimIndent() + ) + + val editorConfigProperties = editorConfigLoader.loadPropertiesForFile( + filePath = null, + alternativeEditorConfig = fileSystemMock.normalizedPath(anotherDir).resolve(".editorconfig"), + isStdIn = true, + rules = rules + ) + val parsedEditorConfig = editorConfigProperties.convertToRawValues() + + assertThat(parsedEditorConfig).isNotEmpty + assertThat(parsedEditorConfig).isEqualTo( + mapOf( + "end_of_line" to "lf", + "indent_size" to "2", + "tab_width" to "2" + ) + ) + } + + @Test + fun `Should support editorconfig globs when loading properties for file specified under such glob`() { + val projectDir = "/project" + + @Language("EditorConfig") + val editorconfigFile = + """ + [*.{kt,kts}] + insert_final_newline = true + disabled_rules = import-ordering + + [api/*.{kt,kts}] + disabled_rules = class-must-be-internal + """.trimIndent() + fileSystemMock.writeEditorConfigFile(projectDir, editorconfigFile) + + val lintFile = fileSystemMock.normalizedPath(projectDir).resolve("api").resolve("test.kt") + Files.createDirectories(lintFile) + + val editorConfigProperties = editorConfigLoader.loadPropertiesForFile(lintFile, debug = true, rules = rules) + val parsedEditorConfig = editorConfigProperties.convertToRawValues() + + assertThat(parsedEditorConfig).isNotEmpty + assertThat(parsedEditorConfig).isEqualTo( + mapOf( + "insert_final_newline" to "true", + "disabled_rules" to "class-must-be-internal" + ) + ) + } + + @Test + fun `Should add property from override`() { + val projectDir = "/project" + + @Language("EditorConfig") + val editorconfigFile = + """ + [*.{kt,kts}] + disabled_rules=import-ordering, no-wildcard-imports + """.trimIndent() + fileSystemMock.writeEditorConfigFile(projectDir, editorconfigFile) + + val lintFile = fileSystemMock.normalizedPath(projectDir).resolve("test.kts") + + val editorConfigProperties = editorConfigLoader.loadPropertiesForFile( + lintFile, + rules = rules.plus(FinalNewlineRule()), + editorConfigOverride = EditorConfigOverride.from(insertNewLineProperty to "true") + ) + val parsedEditorConfig = editorConfigProperties.convertToRawValues() + + assertThat(parsedEditorConfig).isNotEmpty + assertThat(parsedEditorConfig).isEqualTo( + mapOf( + "disabled_rules" to "import-ordering, no-wildcard-imports", + "insert_final_newline" to "true" + ) + ) + } + + @Test + fun `Should replace property from override`() { + val projectDir = "/project" + + @Language("EditorConfig") + val editorconfigFile = + """ + [*.{kt,kts}] + insert_final_newline = true + """.trimIndent() + fileSystemMock.writeEditorConfigFile(projectDir, editorconfigFile) + + val lintFile = fileSystemMock.normalizedPath(projectDir).resolve("test.kts") + + val editorConfigProperties = editorConfigLoader.loadPropertiesForFile( + lintFile, + rules = rules.plus(FinalNewlineRule()), + editorConfigOverride = EditorConfigOverride.from(insertNewLineProperty to "false") + ) + val parsedEditorConfig = editorConfigProperties.convertToRawValues() + + assertThat(parsedEditorConfig).isNotEmpty + assertThat(parsedEditorConfig).isEqualTo( + mapOf( + "insert_final_newline" to "false" + ) + ) + } + } + + private fun FileSystem.normalizedPath(path: String): Path { + val root = rootDirectories.joinToString(separator = "/") + return getPath("$root$path") + } + + private fun FileSystem.writeEditorConfigFile( + filePath: String, + content: String + ) { + Files.createDirectories(normalizedPath(filePath)) + Files.write(normalizedPath("$filePath/.editorconfig"), content.toByteArray()) + } + private class TestRule : Rule("editorconfig-test"), UsesEditorConfigProperties { override val editorConfigProperties: List> = emptyList() diff --git a/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/FileUtils.kt b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/FileUtils.kt index 570cb2edfe..2d741a163f 100644 --- a/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/FileUtils.kt +++ b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/FileUtils.kt @@ -3,6 +3,7 @@ package com.pinterest.ktlint.internal import com.pinterest.ktlint.core.KtLint import com.pinterest.ktlint.core.LintError import com.pinterest.ktlint.core.RuleProvider +import com.pinterest.ktlint.core.api.EditorConfigDefaults import com.pinterest.ktlint.core.api.EditorConfigOverride import com.pinterest.ktlint.core.initKtLintKLogger import java.io.File @@ -98,10 +99,10 @@ internal fun FileSystem.fileSequence( if (negatedPathMatchers.none { it.matches(filePath) } && pathMatchers.any { it.matches(filePath) } ) { - logger.debug { "- File: $filePath: Include" } + logger.trace { "- File: $filePath: Include" } result.add(filePath) } else { - logger.debug { "- File: $filePath: Ignore" } + logger.trace { "- File: $filePath: Ignore" } } return FileVisitResult.CONTINUE } @@ -111,17 +112,17 @@ internal fun FileSystem.fileSequence( dirAttr: BasicFileAttributes ): FileVisitResult { return if (Files.isHidden(dirPath)) { - logger.debug { "- Dir: $dirPath: Ignore" } + logger.trace { "- Dir: $dirPath: Ignore" } FileVisitResult.SKIP_SUBTREE } else { - logger.debug { "- Dir: $dirPath: Traverse" } + logger.trace { "- Dir: $dirPath: Traverse" } FileVisitResult.CONTINUE } } } ) } - logger.debug { "Results: include ${result.count()} files in $duration ms" } + logger.debug { "Discovered ${result.count()} files to be processed in $duration ms" } return result.asSequence() } @@ -196,7 +197,7 @@ internal fun JarFiles.toFilesURIList() = map { // a complete solution would be to implement https://www.gnu.org/software/bash/manual/html_node/Tilde-Expansion.html // this implementation takes care only of the most commonly used case (~/) -private fun String.expandTildeToFullPath(): String = +internal fun String.expandTildeToFullPath(): String = if (os.startsWith("windows", true)) { // Windows sometimes inserts `~` into paths when using short directory names notation, e.g. `C:\Users\USERNA~1\Documents this @@ -215,6 +216,7 @@ internal fun lintFile( fileName: String, fileContents: String, ruleProviders: Set, + editorConfigDefaults: EditorConfigDefaults, editorConfigOverride: EditorConfigOverride, editorConfigPath: String? = null, debug: Boolean = false, @@ -225,6 +227,7 @@ internal fun lintFile( text = fileContents, ruleProviders = ruleProviders, script = !fileName.endsWith(".kt", ignoreCase = true), + editorConfigDefaults = editorConfigDefaults, editorConfigOverride = editorConfigOverride, editorConfigPath = editorConfigPath, cb = { e, _ -> @@ -242,6 +245,7 @@ internal fun formatFile( fileName: String, fileContents: String, ruleProviders: Set, + editorConfigDefaults: EditorConfigDefaults, editorConfigOverride: EditorConfigOverride, editorConfigPath: String?, debug: Boolean, @@ -253,6 +257,7 @@ internal fun formatFile( text = fileContents, ruleProviders = ruleProviders, script = !fileName.endsWith(".kt", ignoreCase = true), + editorConfigDefaults = editorConfigDefaults, editorConfigOverride = editorConfigOverride, editorConfigPath = editorConfigPath, cb = cb, diff --git a/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/KtlintCommandLine.kt b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/KtlintCommandLine.kt index 2296d900c2..fae50d4afa 100644 --- a/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/KtlintCommandLine.kt +++ b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/KtlintCommandLine.kt @@ -13,6 +13,7 @@ import com.pinterest.ktlint.core.api.Baseline.Status.INVALID import com.pinterest.ktlint.core.api.Baseline.Status.NOT_FOUND import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.codeStyleSetProperty import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.disabledRulesProperty +import com.pinterest.ktlint.core.api.EditorConfigDefaults import com.pinterest.ktlint.core.api.EditorConfigOverride import com.pinterest.ktlint.core.api.EditorConfigOverride.Companion.plus import com.pinterest.ktlint.core.api.doesNotContain @@ -27,6 +28,7 @@ import java.io.PrintStream import java.net.URLClassLoader import java.net.URLDecoder import java.nio.file.FileSystems +import java.nio.file.Paths import java.util.Locale import java.util.ServiceLoader import java.util.concurrent.ArrayBlockingQueue @@ -236,6 +238,12 @@ internal class KtlintCommandLine { } }.lintErrorsPerFile } + + val editorConfigDefaults = EditorConfigDefaults.load( + editorConfigPath + ?.expandTildeToFullPath() + ?.let { path -> Paths.get(path) } + ) val editorConfigOverride = EditorConfigOverride .emptyEditorConfigOverride @@ -247,10 +255,16 @@ internal class KtlintCommandLine { reporter.beforeAll() if (stdin) { - lintStdin(ruleProviders, editorConfigOverride, reporter) + lintStdin( + ruleProviders, + editorConfigDefaults, + editorConfigOverride, + reporter + ) } else { lintFiles( ruleProviders, + editorConfigDefaults, editorConfigOverride, baselineLintErrorsPerFile, reporter @@ -281,6 +295,7 @@ internal class KtlintCommandLine { private fun lintFiles( ruleProviders: Set, + editorConfigDefaults: EditorConfigDefaults, editorConfigOverride: EditorConfigOverride, lintErrorsPerFile: Map>, reporter: Reporter @@ -294,6 +309,7 @@ internal class KtlintCommandLine { file to process( fileName = file.path, fileContent = file.readText(), + editorConfigDefaults = editorConfigDefaults, editorConfigOverride = editorConfigOverride, baselineLintErrors = lintErrorsPerFile.getOrDefault(file.relativeRoute, emptyList()), ruleProviders = ruleProviders @@ -304,6 +320,7 @@ internal class KtlintCommandLine { private fun lintStdin( ruleProviders: Set, + editorConfigDefaults: EditorConfigDefaults, editorConfigOverride: EditorConfigOverride, reporter: Reporter ) { @@ -312,6 +329,7 @@ internal class KtlintCommandLine { process( fileName = KtLint.STDIN_FILE, fileContent = String(System.`in`.readBytes()), + editorConfigDefaults = editorConfigDefaults, editorConfigOverride = editorConfigOverride, baselineLintErrors = emptyList(), ruleProviders = ruleProviders @@ -360,6 +378,7 @@ internal class KtlintCommandLine { private fun process( fileName: String, fileContent: String, + editorConfigDefaults: EditorConfigDefaults, editorConfigOverride: EditorConfigOverride, baselineLintErrors: List, ruleProviders: Set @@ -375,6 +394,7 @@ internal class KtlintCommandLine { fileName, fileContent, ruleProviders, + editorConfigDefaults, editorConfigOverride, editorConfigPath, debug @@ -404,6 +424,7 @@ internal class KtlintCommandLine { fileName, fileContent, ruleProviders, + editorConfigDefaults, editorConfigOverride, editorConfigPath, debug diff --git a/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/PrintASTSubCommand.kt b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/PrintASTSubCommand.kt index 925d31c9a5..065e2b6acd 100644 --- a/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/PrintASTSubCommand.kt +++ b/ktlint/src/main/kotlin/com/pinterest/ktlint/internal/PrintASTSubCommand.kt @@ -3,6 +3,7 @@ package com.pinterest.ktlint.internal import com.pinterest.ktlint.core.KtLint import com.pinterest.ktlint.core.ParseException import com.pinterest.ktlint.core.RuleProvider +import com.pinterest.ktlint.core.api.EditorConfigDefaults.Companion.emptyEditorConfigDefaults import com.pinterest.ktlint.core.api.EditorConfigOverride.Companion.emptyEditorConfigOverride import com.pinterest.ktlint.core.initKtLintKLogger import com.pinterest.ruleset.test.DumpASTRule @@ -75,6 +76,7 @@ internal class PrintASTSubCommand : Runnable { ruleProviders = setOf( RuleProvider { DumpASTRule(System.out, ktlintCommand.color) } ), + editorConfigDefaults = emptyEditorConfigDefaults, editorConfigOverride = emptyEditorConfigOverride, debug = ktlintCommand.debug ) diff --git a/ktlint/src/test/kotlin/com/pinterest/ktlint/BaseCLITest.kt b/ktlint/src/test/kotlin/com/pinterest/ktlint/BaseCLITest.kt index 0cdbbb399d..d47d5f8718 100644 --- a/ktlint/src/test/kotlin/com/pinterest/ktlint/BaseCLITest.kt +++ b/ktlint/src/test/kotlin/com/pinterest/ktlint/BaseCLITest.kt @@ -9,6 +9,7 @@ import java.nio.file.SimpleFileVisitor import java.nio.file.attribute.BasicFileAttributes import java.util.concurrent.TimeUnit import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.fail import org.junit.jupiter.api.io.TempDir abstract class BaseCLITest { @@ -34,21 +35,42 @@ abstract class BaseCLITest { it.replace(BASE_DIR_PLACEHOLDER, tempDir.toString()) } // Forking in a new shell process, so 'ktlint' will pickup new 'PATH' env variable value - val pb = ProcessBuilder("/bin/sh", "-c", ktlintCommand) - pb.directory(projectPath.toAbsolutePath().toFile()) + val processBuilder = ProcessBuilder("/bin/sh", "-c", ktlintCommand) + processBuilder.directory(projectPath.toAbsolutePath().toFile()) // Overriding user path to java executable to use java version test is running on - val environment = pb.environment() + val environment = processBuilder.environment() environment["PATH"] = "${System.getProperty("java.home")}${File.separator}bin${File.pathSeparator}${System.getenv()["PATH"]}" - val process = pb.start() - val output = process.inputStream.bufferedReader().use { it.readLines() } - val error = process.errorStream.bufferedReader().use { it.readLines() } - process.waitFor(WAIT_TIME_SEC, TimeUnit.SECONDS) + val process = processBuilder.start() + if (process.completedInAllowedDuration()) { + val output = process.inputStream.bufferedReader().use { it.readLines() } + val error = process.errorStream.bufferedReader().use { it.readLines() } - executionAssertions(ExecutionResult(process.exitValue(), output, error, projectPath)) + executionAssertions(ExecutionResult(process.exitValue(), output, error, projectPath)) - process.destroy() + // Destroy process only after output is collected as other the streams are not completed. + process.destroy() + } else { + // Destroy before failing the test as the process otherwise keeps running + process.destroyForcibly() + + val maxDurationInSeconds = (WAIT_INTERVAL_DURATION * WAIT_INTERVAL_MAX_OCCURRENCES).div(1000.0) + fail { + "CLI test has been aborted as it could not be completed in $maxDurationInSeconds seconds" + } + } + } + + private fun Process.completedInAllowedDuration(): Boolean { + (0..WAIT_INTERVAL_MAX_OCCURRENCES).forEach { _ -> + if (isAlive) { + waitFor(WAIT_INTERVAL_DURATION, TimeUnit.MILLISECONDS) + } else { + return true + } + } + return false } private fun prepareTestProject(testProjectName: String): Path { @@ -128,7 +150,8 @@ abstract class BaseCLITest { } companion object { - private const val WAIT_TIME_SEC = 3L + private const val WAIT_INTERVAL_DURATION = 100L + private const val WAIT_INTERVAL_MAX_OCCURRENCES = 100 val testProjectsPath: Path = Paths.get("src", "test", "resources", "cli") const val BASE_DIR_PLACEHOLDER = "__TEMP_DIR__" } diff --git a/ktlint/src/test/kotlin/com/pinterest/ktlint/EditorConfigDefaultsLoaderCLITest.kt b/ktlint/src/test/kotlin/com/pinterest/ktlint/EditorConfigDefaultsLoaderCLITest.kt new file mode 100644 index 0000000000..9da2402acc --- /dev/null +++ b/ktlint/src/test/kotlin/com/pinterest/ktlint/EditorConfigDefaultsLoaderCLITest.kt @@ -0,0 +1,107 @@ +package com.pinterest.ktlint + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.ListAssert +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.DisabledOnOs +import org.junit.jupiter.api.condition.OS +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +@DisabledOnOs(OS.WINDOWS) +@DisplayName("CLI default editorconfig loading") +class EditorConfigDefaultsLoaderCLITest : BaseCLITest() { + @Test + fun `When no default editorconfig is specified only the normal editorconfig file(s) on the file paths are used`() { + runKtLintCliProcess( + "editorconfig-path", + listOf() + ) { + assertErrorExitCode() + + val assertThat = assertThat(normalOutput) + assertThat + .containsLineMatching(Regex(".*FooTest.*Exceeded max line length \\(30\\).*")) + .containsLineMatching(Regex(".*Foo.*Exceeded max line length \\(30\\).*")) + .containsLineMatching(Regex(".*foobar2.*File name 'foobar2.kt' should conform PascalCase.*")) + // The Bar files are not matched by any glob + .doesNotContainLineMatching(Regex(".*Bar.*")) + // The filename rule is disabled for the examples-directory only + .doesNotContainLineMatching(Regex(".*foobar1.*File name 'foobar1.kt' should conform PascalCase.*")) + } + } + + @ParameterizedTest(name = "Alternative editorconfig file: {0}") + @ValueSource( + strings = [ + ".editorconfig-bar", + "editorconfig-alternative", + // Ensure that relative urls are allowed as well + "../project/editorconfig-alternative" + ] + ) + fun `Given a default editorconfig path then use defaults when editorconfig files on the filepath do not resolve the property`( + editorconfigPath: String + ) { + val projectDirectory = "$BASE_DIR_PLACEHOLDER/editorconfig-path/project" + runKtLintCliProcess( + "editorconfig-path", + listOf("--editorconfig=$projectDirectory/$editorconfigPath") + ) { + assertErrorExitCode() + + val assertThat = assertThat(normalOutput) + assertThat + .containsLineMatching(Regex(".*FooTest.*Exceeded max line length \\(30\\).*")) + .containsLineMatching(Regex(".*Foo.*Exceeded max line length \\(30\\).*")) + // Only the Bar-files fall back on the default editorconfig! + .containsLineMatching(Regex(".*BarTest.*Exceeded max line length \\(20\\).*")) + .containsLineMatching(Regex(".*Bar.*Exceeded max line length \\(20\\).*")) + } + } + + @Test + fun `Given that the default editorconfig sets the default max line length for Test files only then use defaults when editorconfig files on the filepath do not resolve the property`() { + val projectDirectory = "$BASE_DIR_PLACEHOLDER/editorconfig-path/project" + runKtLintCliProcess( + "editorconfig-path", + listOf("--editorconfig=$projectDirectory/.editorconfig-default-max-line-length-on-tests-only") + ) { + assertErrorExitCode() + + val assertThat = assertThat(normalOutput) + assertThat + .containsLineMatching(Regex(".*FooTest.*Exceeded max line length \\(30\\).*")) + .containsLineMatching(Regex(".*Foo.*Exceeded max line length \\(30\\).*")) + // Only the BarTest-file falls back on the default editorconfig! + .containsLineMatching(Regex(".*BarTest.*Exceeded max line length \\(25\\).*")) + } + } + + @Test + fun `Given that the default editorconfig disables the filename rule for all example files`() { + val projectDirectory = "$BASE_DIR_PLACEHOLDER/editorconfig-path/project" + runKtLintCliProcess( + "editorconfig-path", + listOf("--editorconfig=$projectDirectory/.editorconfig-disable-filename-rule") + ) { + assertErrorExitCode() + + val assertThat = assertThat(normalOutput) + assertThat + .doesNotContainLineMatching(Regex(".*foobar1.*File name 'foobar1.kt' should conform PascalCase.*")) + .doesNotContainLineMatching(Regex(".*foobar2.*File name 'foobar2.kt' should conform PascalCase.*")) + } + } + + private fun ListAssert.containsLineMatching(regex: Regex) = + this.anyMatch { + it.matches(regex) + } + + private fun ListAssert.doesNotContainLineMatching(regex: Regex) = + this.noneMatch { + it.matches(regex) + } +} diff --git a/ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig b/ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig new file mode 100644 index 0000000000..c4a80577df --- /dev/null +++ b/ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[Foo*.{kt,kts}] +max_line_length = 30 + +[src/**/example/*.kt] +ktlint_disabled_rules = filename diff --git a/ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig-bar b/ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig-bar new file mode 100644 index 0000000000..e99e59e4ce --- /dev/null +++ b/ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig-bar @@ -0,0 +1,4 @@ +root = true + +[Bar*.{kt,kts}] +max_line_length = 20 diff --git a/ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig-default-max-line-length-on-tests-only b/ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig-default-max-line-length-on-tests-only new file mode 100644 index 0000000000..8c489f851e --- /dev/null +++ b/ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig-default-max-line-length-on-tests-only @@ -0,0 +1,4 @@ +root = true + +[*Test.{kt,kts}] +max_line_length = 25 diff --git a/ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig-disable-filename-rule b/ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig-disable-filename-rule new file mode 100644 index 0000000000..5b09b87dae --- /dev/null +++ b/ktlint/src/test/resources/cli/editorconfig-path/project/.editorconfig-disable-filename-rule @@ -0,0 +1,4 @@ +root = true + +[**/*-example/*.{kt,kts}] +ktlint_disabled_rules = filename diff --git a/ktlint/src/test/resources/cli/editorconfig-path/project/editorconfig-alternative b/ktlint/src/test/resources/cli/editorconfig-path/project/editorconfig-alternative new file mode 100644 index 0000000000..e99e59e4ce --- /dev/null +++ b/ktlint/src/test/resources/cli/editorconfig-path/project/editorconfig-alternative @@ -0,0 +1,4 @@ +root = true + +[Bar*.{kt,kts}] +max_line_length = 20 diff --git a/ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/example/Bar.kts b/ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/example/Bar.kts new file mode 100644 index 0000000000..52d07cb713 --- /dev/null +++ b/ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/example/Bar.kts @@ -0,0 +1 @@ +val bar = "barbarbarbarbarbarbarbarbarbarbarbarbar" diff --git a/ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/example/Foo.kt b/ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/example/Foo.kt new file mode 100644 index 0000000000..177566bb2d --- /dev/null +++ b/ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/example/Foo.kt @@ -0,0 +1 @@ +val foo = "fooooooooooooooooooooooooooooooooooooooooo" diff --git a/ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/example/foobar1.kt b/ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/example/foobar1.kt new file mode 100644 index 0000000000..56d619f32e --- /dev/null +++ b/ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/example/foobar1.kt @@ -0,0 +1 @@ +fun foobar1() {} diff --git a/ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/filename-example/foobar2.kt b/ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/filename-example/foobar2.kt new file mode 100644 index 0000000000..23b1d03d3e --- /dev/null +++ b/ktlint/src/test/resources/cli/editorconfig-path/project/src/main/kotlin/filename-example/foobar2.kt @@ -0,0 +1 @@ +fun foobar2() {} diff --git a/ktlint/src/test/resources/cli/editorconfig-path/project/src/test/kotlin/example/BarTest.kts b/ktlint/src/test/resources/cli/editorconfig-path/project/src/test/kotlin/example/BarTest.kts new file mode 100644 index 0000000000..f4585fdb36 --- /dev/null +++ b/ktlint/src/test/resources/cli/editorconfig-path/project/src/test/kotlin/example/BarTest.kts @@ -0,0 +1 @@ +val barTest = "barbarbarbarbarbarbarbarbarbarbarbarbar" diff --git a/ktlint/src/test/resources/cli/editorconfig-path/project/src/test/kotlin/example/FooTest.kt b/ktlint/src/test/resources/cli/editorconfig-path/project/src/test/kotlin/example/FooTest.kt new file mode 100644 index 0000000000..e4e7518ba7 --- /dev/null +++ b/ktlint/src/test/resources/cli/editorconfig-path/project/src/test/kotlin/example/FooTest.kt @@ -0,0 +1 @@ +val fooTest = "fooooooooooooooooooooooooooooooooooooooooo" diff --git a/ktlint/src/test/resources/test-baseline.xml b/ktlint/src/test/resources/test-baseline.xml index f5a5e832f3..e0443e3c95 100644 --- a/ktlint/src/test/resources/test-baseline.xml +++ b/ktlint/src/test/resources/test-baseline.xml @@ -7,4 +7,13 @@ + + + + + + + + +