Skip to content

Commit

Permalink
Improve support of default editorconfig properties
Browse files Browse the repository at this point in the history
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 pinterest#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 13, 2022
1 parent 6fdb80e commit 54f8194
Show file tree
Hide file tree
Showing 28 changed files with 1,193 additions and 175 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.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)).
Expand All @@ -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))
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
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,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)
}

0 comments on commit 54f8194

Please sign in to comment.