Skip to content

Commit

Permalink
Merge pull request #874 from Tapchicoma/701/initial-editorconfig-inte…
Browse files Browse the repository at this point in the history
…gration

Initial implementation of IDE integration via editorconfig
  • Loading branch information
Tapchicoma committed Sep 18, 2020
2 parents 683d088 + 30cfe18 commit f305e2e
Show file tree
Hide file tree
Showing 16 changed files with 695 additions and 103 deletions.
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())
}
}

0 comments on commit f305e2e

Please sign in to comment.