Skip to content

Commit

Permalink
Improve support of default editorconfig properties (#1580)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
paul-dingemans committed Aug 18, 2022
1 parent b74ac02 commit 3c6eaca
Show file tree
Hide file tree
Showing 29 changed files with 1,200 additions and 176 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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.editorConfigPath" 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.

#### Miscellaneous

Several methods for which it is unlikely that they are used by API consumers have been marked for removal from the public API in KtLint 0.48.0. Please create an issue in case you have a valid business case to keep such methods in the public API.
Expand All @@ -153,6 +160,7 @@ Several methods for which it is unlikely that they are used by API consumers hav
* Handle trailing comma in enums `trailing-comma` ([#1542](https://github.com/pinterest/ktlint/pull/1542))
* Allow EOL comment after annotation ([#1539](https://github.com/pinterest/ktlint/issues/1539))
* 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))
* Fix indent of when entry with a dot qualified expression instead of simple value when trailing comma is required ([#1519](https://github.com/pinterest/ktlint/pull/1519))
* Fix whitespace between trailing comma and arrow in when entry when trailing comma is required ([#1519](https://github.com/pinterest/ktlint/pull/1519))
* Prevent false positive in parameter list for which the last value parameter is a destructuring declaration followed by a trailing comma `wrapping` ([#1578](https://github.com/pinterest/ktlint/issues/1578))
Expand All @@ -163,6 +171,7 @@ Several methods for which it is unlikely that they are used by API consumers hav
* Invoke callback on `format` function for all errors including errors that are autocorrected ([#1491](https://github.com/pinterest/ktlint/issues/1491))
* Improve rule `annotation` ([#1574](https://github.com/pinterest/ktlint/pull/1574))
* 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" ([#1580](https://github.com/pinterest/ktlint/pull/1580))
* Update Kotlin development version to `1.7.20-beta` and Kotlin version to `1.7.10`.
* Update release scripting to set version number in mkdocs documentation ([#1575](https://github.com/pinterest/ktlint/issue/1575)).

Expand Down
13 changes: 8 additions & 5 deletions build.gradle
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/install/cli.md
Expand Up @@ -134,14 +134,14 @@ ktlint --experimental --ruleset=/path/to/custom-ruleset.jar generateEditorConfig

Normally this file is located in the root of your project directory. In case the file is located in a sub folder of the project, the settings of that file only applies to that subdirectory and its folders (recursively). Ktlint automatically detects and reads all `.editorconfig` files in your project.

With command below, an `editorconfig` file of an alternative location can be used to configure ktlint:
Use command below, to specify a default `editorconfig`. In case a property is not defined in any `.editorconfig` file on the path to the file, the value from the default file is used. The path may point to any valid file or directory. The path can be relative or absolute. Depending on your OS, the "~" at the beginning of a path is replaced by the user home directory.

```shell title="Override '.editorconfig'"
ktlint --editorconfig=/path/to/.editorconfig
```

!!! warning "Overrides '.editorconfig' in project directory"
When specifying this option, all `.editorconfig` files in the project directory are being ignored.
!!! warning "Overrides '.editorconfig' in project directory" in KtLint 0.46 and older
When specifying this option using ktlint 0.46 or older, all `.editorconfig` files in the project directory are being ignored. Starting from KtLint 0.47 the properties in this file are used as fallback.

### Stdin && stdout

Expand Down
18 changes: 14 additions & 4 deletions ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
*
Expand All @@ -70,8 +78,10 @@ public object KtLint {
val userData: Map<String, String> = 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,
) {
Expand Down Expand Up @@ -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()
}

/**
Expand Down
@@ -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())
}
}
Expand Up @@ -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
Expand All @@ -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."
}
}
}
Expand Down Expand Up @@ -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 -> {
Expand All @@ -308,7 +305,7 @@ public object DefaultEditorConfigProperties : UsesEditorConfigProperties {
}
}
property.sourceValue == "off" -> -1
else -> property.getValueAs()
else -> PropertyType.max_line_length.parse(property.sourceValue).parsed
}
},
)
Expand Down
@@ -0,0 +1,73 @@
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.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 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)
}

0 comments on commit 3c6eaca

Please sign in to comment.