Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of IDE integration via editorconfig #874

Merged
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Added
- Initial implementation IDE integration via '.editorconfig' based on rules default values ([#701](https://github.com/pinterest/ktlint/issues/701))

### Fixed
- ?

### Changed
- ?

### Removed
- ?

## [0.39.0] - 2020-09-14

### Added
Expand Down
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ allprojects {
kotlinOptions {
jvmTarget = '1.8'
apiVersion = '1.3'

freeCompilerArgs = ["-Xopt-in=kotlin.RequiresOptIn"]
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion ktlint-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ dependencies {
api deps.kotlin.compiler

implementation deps.kotlin.stdlib
implementation deps.ec4j
api deps.ec4j

testImplementation deps.junit
testImplementation deps.assertj
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ interface EditorConfig {
val indentSize: Int
val tabWidth: Int
val maxLineLength: Int
@Deprecated(
message = "Not used anymore by rules, please use 'insert_final_newline' directly via get()"
)
val insertFinalNewline: Boolean
fun get(key: String): String?

operator fun get(key: String): String?

companion object {
fun fromMap(map: Map<String, String>): EditorConfig {
Expand Down
65 changes: 61 additions & 4 deletions ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.pinterest.ktlint.core

import com.pinterest.ktlint.core.api.EditorConfigProperties
import com.pinterest.ktlint.core.api.FeatureInAlphaState
import com.pinterest.ktlint.core.ast.visit
import com.pinterest.ktlint.core.internal.EditorConfigGenerator
import com.pinterest.ktlint.core.internal.EditorConfigLoader
import com.pinterest.ktlint.core.internal.EditorConfigLoader.Companion.convertToRawValues
import com.pinterest.ktlint.core.internal.LineAndColumn
import com.pinterest.ktlint.core.internal.SuppressionLocator
import com.pinterest.ktlint.core.internal.buildPositionInTextLocator
Expand All @@ -25,13 +29,18 @@ public object KtLint {
public val EDITOR_CONFIG_USER_DATA_KEY: Key<EditorConfig> = Key<EditorConfig>("EDITOR_CONFIG")
public val ANDROID_USER_DATA_KEY: Key<Boolean> = Key<Boolean>("ANDROID")
public val FILE_PATH_USER_DATA_KEY: Key<String> = Key<String>("FILE_PATH")
public val EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY: Key<EditorConfigProperties> =
Key<EditorConfigProperties>("EDITOR_CONFIG_PROPERTIES")
public val DISABLED_RULES: Key<Set<String>> = Key<Set<String>>("DISABLED_RULES")
private const val UTF8_BOM = "\uFEFF"
public const val STDIN_FILE: String = "<stdin>"

private val psiFileFactory: PsiFileFactory = initPsiFileFactory()
private val editorConfigLoader = EditorConfigLoader(FileSystems.getDefault())

@OptIn(FeatureInAlphaState::class)
private val editorConfigGenerator = EditorConfigGenerator(editorConfigLoader)

/**
* @param fileName path of file to lint/format
* @param text Contents of file to lint/format
Expand Down Expand Up @@ -59,6 +68,12 @@ public object KtLint {
}

internal val isStdIn: Boolean get() = fileName == STDIN_FILE

internal val rules: Set<Rule> get() = ruleSets
.flatMap {
it.rules.toList()
}
.toSet()
}

/**
Expand Down Expand Up @@ -121,14 +136,21 @@ public object KtLint {

val rootNode = psiFile.node

// Passed-in userData overrides .editorconfig
val mergedUserData = editorConfigLoader.loadPropertiesForFile(
val editorConfigProperties = editorConfigLoader.loadPropertiesForFile(
params.normalizedFilePath,
params.isStdIn,
params.editorConfigPath?.let { Paths.get(it) },
params.rules,
params.debug
)

// Passed-in userData overrides .editorconfig
val mergedUserData = editorConfigProperties.convertToRawValues(
params.normalizedFilePath,
params.isStdIn
) + params.userData
injectUserData(rootNode, mergedUserData)

injectUserData(rootNode, editorConfigProperties, mergedUserData)

val suppressedRegionLocator = buildSuppressedRegionsLocator(rootNode)

Expand All @@ -150,7 +172,11 @@ public object KtLint {
.replaceFirst(UTF8_BOM, "")
}

private fun injectUserData(node: ASTNode, userData: Map<String, String>) {
private fun injectUserData(
node: ASTNode,
editorConfigProperties: EditorConfigProperties,
userData: Map<String, String>
) {
val android = userData["android"]?.toBoolean() ?: false
val editorConfigMap =
if (android &&
Expand All @@ -162,6 +188,7 @@ public object KtLint {
}
node.putUserData(FILE_PATH_USER_DATA_KEY, userData["file_path"])
node.putUserData(EDITOR_CONFIG_USER_DATA_KEY, EditorConfig.fromMap(editorConfigMap - "android" - "file_path"))
node.putUserData(EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY, editorConfigProperties)
node.putUserData(ANDROID_USER_DATA_KEY, android)
node.putUserData(
DISABLED_RULES,
Expand Down Expand Up @@ -336,6 +363,36 @@ public object KtLint {
editorConfigLoader.trimMemory()
}

/**
* Generates Kotlin `.editorconfig` file section content based on [Params.ruleSets].
*
* Method loads merged `.editorconfig` content from [Params.fileName] path,
* and then, by querying rules from [Params.ruleSets] for missing properties default values,
* generates Kotlin section (default is `[*.{kt,kts}]`) new content.
*
* Rule should implement [UsesEditorConfigProperties] interface to support this.
*
* @return Kotlin section editorconfig content. For example:
* ```properties
* final-newline=true
* indent-size=4
* ```
*/
@FeatureInAlphaState
public fun generateKotlinEditorConfigSection(
params: Params
): String {
val filePath = params.normalizedFilePath
requireNotNull(filePath) {
"Please pass path to existing Kotlin file"
}
return editorConfigGenerator.generateEditorconfig(
filePath,
params.rules,
params.debug
)
}

private fun determineLineSeparator(fileContent: String, userData: Map<String, String>): String {
val eol = userData["end_of_line"]?.trim()?.toLowerCase()
return when {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.pinterest.ktlint.core.api

@RequiresOptIn(
message = "This Ktlint feature is highly experimental, and most probably will change in the future releases.",
level = RequiresOptIn.Level.ERROR
)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
public annotation class FeatureInAlphaState

@RequiresOptIn(
message = "This Ktlint feature is experimental, and may change in the future releases.",
level = RequiresOptIn.Level.WARNING
)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
public annotation class FeatureInBetaState
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.pinterest.ktlint.core.api

import org.ec4j.core.model.Property
import org.ec4j.core.model.PropertyType

/**
* Indicates [com.pinterest.ktlint.core.Rule] uses properties loaded from `.editorconfig` file.
*
* This properties could be:
* - universal `.editorconfig` properties defined
* [here](https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#current-universal-properties)
* - universal IntelliJ IDEA properties defined
* [here](https://github.com/JetBrains/intellij-community/blob/master/platform/lang-api/src/com/intellij/psi/codeStyle/CommonCodeStyleSettings.java)
* - Kotlin specific properties defined
* [here](https://github.com/JetBrains/kotlin/blob/master/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinCodeStyleSettings.java)
*
* In the best case rule should only use one property.
*
* See [com.pinterest.ktlint.core.KtLint.generateKotlinEditorConfigSection] documentation how to generate
* `.editorconfig` based on [com.pinterest.ktlint.core.Rule]s with this interface implementations.
*/
@FeatureInAlphaState
public interface UsesEditorConfigProperties {

/**
* Provide a list of code style editorconfig properties, that rule uses in linting.
*/
public val editorConfigProperties: List<EditorConfigProperty<*>>

/**
* Get the value of [EditorConfigProperty] based on loaded [EditorConfigProperties] content.
*/
public fun <T> EditorConfigProperties.getEditorConfigValue(property: EditorConfigProperty<T>): T {
return get(property.type.name)?.getValueAs() ?: property.defaultValue
}

/**
* Supported `.editorconfig` property.
*
* @param type type of property. Could be one of default ones (see [PropertyType.STANDARD_TYPES]) or custom one.
* @param defaultValue default value for property if it does not exist in loaded properties.
*/
public data class EditorConfigProperty<T>(
public val type: PropertyType<T>,
public val defaultValue: T
)
}

/**
* Loaded [Property]s from `.editorconfig` files.
*/
public typealias EditorConfigProperties = Map<String, Property>
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.pinterest.ktlint.core.internal

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.api.FeatureInAlphaState
import com.pinterest.ktlint.core.api.UsesEditorConfigProperties
import java.nio.file.Path
import org.ec4j.core.model.Property

/**
* Generates Kotlin section content for `.editorconfig` file.
*
* Rule should implement [UsesEditorConfigProperties] interface to support this.
*/
@FeatureInAlphaState
internal class EditorConfigGenerator(
private val editorConfigLoader: EditorConfigLoader
) {
/**
* Method loads merged `.editorconfig` content using [com.pinterest.ktlint.core.KtLint.Params.fileName] path,
* and then, by querying rules from [com.pinterest.ktlint.core.KtLint.Params.ruleSets]
* generates Kotlin section (default is `[*.{kt,kts}]`) content including expected default values.
*
* @return Kotlin section editorconfig content. For example:
* ```properties
* final-newline = true
* indent-size = 4
* ```
*/
fun generateEditorconfig(
filePath: Path,
rules: Set<Rule>,
debug: Boolean = false
): String {
val editorConfig: Map<String, Property> = editorConfigLoader.loadPropertiesForFile(
filePath = filePath,
rules = rules,
debug = debug
)

return rules
.fold(mutableMapOf<String, String?>()) { acc, rule ->
if (rule is UsesEditorConfigProperties) {
if (debug) println("Checking properties for '${rule.id}' rule")
rule.editorConfigProperties.forEach { prop ->
if (debug) println("Setting '${prop.type.name}' property value")
acc[prop.type.name] = with(rule) { editorConfig.getEditorConfigValue(prop).toString() }
}
}

acc
}
.filterValues { it != null }
.map {
"${it.key} = ${it.value}"
}
.joinToString(separator = System.lineSeparator())
}
}