Skip to content

Commit af60c47

Browse files
tKemeta-codesync[bot]
authored andcommitted
feat: --enable-editorconfig flag and .editorconfig support (#570)
Summary: closes #45 Pull Request resolved: #570 Reviewed By: cortinico Differential Revision: D88870046 Pulled By: hick209 fbshipit-source-id: 1365a914e49c325bc72146739a35e796e7d7052a
1 parent 3dc2eed commit af60c47

9 files changed

Lines changed: 453 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
1515
### Added
1616
- Support for `else if` guard conditions (https://github.com/facebook/ktfmt/pull/563)
1717
- Explicit Kotlin import layout for the default and Google specific editorconfig files to match ktfmt's style. The same layout was already applied to the Kotlin Lang editorconfig (https://github.com/facebook/ktfmt/pull/571)
18+
- ktfmt cli can pull formatting configs from editor config files (https://github.com/facebook/ktfmt/pull/570)
1819

1920

2021
## [0.59]

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,20 @@ $ java -jar /path/to/ktfmt-<VERSION>-with-dependencies.jar [--kotlinlang-style |
7878
`--kotlinlang-style` makes `ktfmt` use a block indent of 4 spaces instead of 2.
7979
See below for details.
8080

81+
`--enable-editorconfig` makes `ktfmt` enables limited support to override the style's configuration based on the
82+
following subset of editorconfig properties:
83+
84+
| EditorConfig Property | Description |
85+
|:----------------------------------------------------------|--------------------------------------------------------------------------------------------------|
86+
| `max_line_length` | will override the max line width |
87+
| `indent_size`<br/>*or `tab_width` if `indent_size = tab`* | will override the block indent |
88+
| `ij_continuation_indent_size` | will override the continuation indent |
89+
| `ktfmt_trailing_comma_management_strategy` | one of `none`, `only_add` or `complete`<br/>will override the trailing comma management strategy |
90+
8191
***Note:***
82-
*There is no configurability as to the formatter's algorithm for formatting (apart from the
83-
different styles). This is a deliberate design decision to unify our code formatting on a single
84-
format.*
92+
*There is no configurability as to the formatter's algorithm for formatting (apart from the different styles
93+
or limited `.editorconfig` support). This is a deliberate design decision to unify our code formatting on a
94+
single format.*
8595

8696
### using Gradle
8797

@@ -138,6 +148,8 @@ Two reasons -
138148
However, we do offer an alternative style for projects that absolutely cannot make the move to
139149
`ktfmt` because of 2-space: the style `--kotlinlang-style` changes block indents to 4-space.
140150

151+
Alternatively, the `ktfmt` command-line supports a limited subset of `.editorconfig` properties; see above.
152+
141153
## Developer's Guide
142154

143155
### Setup

core/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies {
3838
api(libs.kotlin.stdlib)
3939
api(libs.kotlin.test)
4040
api(libs.kotlin.compilerEmbeddable)
41+
implementation(libs.ec4j)
4142
testImplementation(libs.googleTruth)
4243
testImplementation(libs.junit)
4344
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.facebook.ktfmt.cli
18+
19+
import com.facebook.ktfmt.format.FormattingOptions
20+
import com.facebook.ktfmt.format.TrailingCommaManagementStrategy
21+
import java.io.File
22+
import java.util.concurrent.ConcurrentHashMap
23+
import org.ec4j.core.EditorConfigLoader
24+
import org.ec4j.core.PropertyTypeRegistry
25+
import org.ec4j.core.Resource
26+
import org.ec4j.core.ResourceProperties
27+
import org.ec4j.core.ResourcePropertiesService
28+
import org.ec4j.core.model.EditorConfig
29+
import org.ec4j.core.model.PropertyType
30+
import org.ec4j.core.model.Version.CURRENT
31+
32+
object EditorConfigResolver {
33+
34+
private val ijContinuationIndentSize: PropertyType<Int> =
35+
PropertyType.LowerCasingPropertyType(
36+
"ij_continuation_indent_size",
37+
"Denotes the continuation indent size. Useful to distinguish code blocks versus continuation lines",
38+
PropertyType.PropertyValueParser.POSITIVE_INT_VALUE_PARSER,
39+
)
40+
41+
private val commaManagementStrategy: PropertyType<TrailingCommaManagementStrategy> =
42+
PropertyType.LowerCasingPropertyType(
43+
"ktfmt_trailing_comma_management_strategy",
44+
"Ktfmt Trailing Comma Management Strategy",
45+
{ _: String, value: String ->
46+
TrailingCommaManagementStrategy.entries
47+
.find { it.name.lowercase() == value }
48+
?.let { PropertyType.PropertyValue.valid(value, it) }
49+
?: PropertyType.PropertyValue.invalid(
50+
value,
51+
"Unknown ktfmt_trailing_comma_management_strategy value '$value'",
52+
)
53+
},
54+
TrailingCommaManagementStrategy.entries.map { it.name.lowercase() }.toSet(),
55+
)
56+
57+
private val propertyTypeRegistry by
58+
lazy(LazyThreadSafetyMode.NONE) {
59+
PropertyTypeRegistry.builder()
60+
.defaults()
61+
.type(PropertyType.max_line_length) // missing from defaults?
62+
.type(ijContinuationIndentSize)
63+
.type(commaManagementStrategy)
64+
.build()
65+
}
66+
67+
private object Cache : org.ec4j.core.Cache {
68+
val cached = ConcurrentHashMap<Resource, EditorConfig>()
69+
70+
override fun get(
71+
editorConfigFile: Resource,
72+
loader: EditorConfigLoader,
73+
): EditorConfig = cached.computeIfAbsent(editorConfigFile, loader::load)
74+
}
75+
76+
private val resourcePropertiesService: ResourcePropertiesService by
77+
lazy(LazyThreadSafetyMode.NONE) {
78+
ResourcePropertiesService.builder()
79+
.cache(Cache)
80+
.loader(EditorConfigLoader.of(CURRENT, propertyTypeRegistry))
81+
.build()
82+
}
83+
84+
fun resolveFormattingOptions(file: File, baseOptions: FormattingOptions): FormattingOptions =
85+
resourcePropertiesService
86+
.queryProperties(Resource.Resources.ofPath(file.toPath(), Charsets.UTF_8))
87+
.resolveFormattingOptions(baseOptions)
88+
89+
private fun ResourceProperties.resolveFormattingOptions(
90+
baseOptions: FormattingOptions,
91+
): FormattingOptions {
92+
// `max_line_length` may return null to indicate 'off', in which case we keep the base maxWidth
93+
val maxWidth =
94+
getValue(PropertyType.max_line_length, baseOptions.maxWidth, false) ?: baseOptions.maxWidth
95+
96+
// `indent_size` may return null to indicate 'tab', in which case we defer to `tab_width`
97+
val blockIndent =
98+
getValue(PropertyType.indent_size, baseOptions.blockIndent, false)
99+
?: getValue(PropertyType.tab_width, baseOptions.blockIndent, false)
100+
101+
val continuationIndent =
102+
getValue(ijContinuationIndentSize, baseOptions.continuationIndent, false)
103+
104+
val trailingCommaStrategy =
105+
getValue(commaManagementStrategy, baseOptions.trailingCommaManagementStrategy, false)
106+
107+
val resolved =
108+
baseOptions.copy(
109+
maxWidth = maxWidth,
110+
blockIndent = blockIndent,
111+
continuationIndent = continuationIndent,
112+
trailingCommaManagementStrategy = trailingCommaStrategy,
113+
)
114+
return resolved
115+
}
116+
}

core/src/main/java/com/facebook/ktfmt/cli/Main.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,12 @@ class Main(
140140
private fun format(file: File?, args: ParsedArgs): Boolean {
141141
val fileName = file?.toString() ?: args.stdinName ?: "<stdin>"
142142
try {
143+
val formattingOptions =
144+
if (file == null || !args.editorConfig) args.formattingOptions
145+
else EditorConfigResolver.resolveFormattingOptions(file, args.formattingOptions)
143146
val bytes = if (file == null) input else FileInputStream(file)
144147
val code = BufferedReader(InputStreamReader(bytes, UTF_8)).readText()
145-
val formattedCode = Formatter.format(args.formattingOptions, code)
148+
val formattedCode = Formatter.format(formattingOptions, code)
146149
val alreadyFormatted = code == formattedCode
147150

148151
// stdin

core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ data class ParsedArgs(
3636
val setExitIfChanged: Boolean,
3737
/** File name to report when formating code from stdin */
3838
val stdinName: String?,
39+
val editorConfig: Boolean,
3940
) {
4041
companion object {
4142

@@ -81,6 +82,8 @@ data class ParsedArgs(
8182
| --set-exit-if-changed Sets exit code to 1 if any input file was not
8283
| formatted/touched
8384
| --do-not-remove-unused-imports Leaves all imports in place, even if not used
85+
| --enable-editorconfig Enable .editorconfig overrides for supported formatting options (limited)
86+
| see https://github.com/facebook/ktfmt/blob/main/README.md
8487
|
8588
|ARGFILE:
8689
| If the only argument begins with '@', the remainder of the argument is treated
@@ -106,6 +109,7 @@ data class ParsedArgs(
106109
var setExitIfChanged = false
107110
var removeUnusedImports = true
108111
var stdinName: String? = null
112+
var editorConfig = false
109113

110114
if ("--help" in args || "-h" in args) return ParseResult.ShowMessage(HELP_TEXT)
111115
if ("--version" in args || "-v" in args) {
@@ -120,6 +124,7 @@ data class ParsedArgs(
120124
arg == "--dry-run" || arg == "-n" -> dryRun = true
121125
arg == "--set-exit-if-changed" -> setExitIfChanged = true
122126
arg == "--do-not-remove-unused-imports" -> removeUnusedImports = false
127+
arg == "--enable-editorconfig" -> editorConfig = true
123128
arg.startsWith("--stdin-name=") ->
124129
stdinName =
125130
parseKeyValueArg("--stdin-name", arg)
@@ -152,6 +157,7 @@ data class ParsedArgs(
152157
dryRun,
153158
setExitIfChanged,
154159
stdinName,
160+
editorConfig,
155161
)
156162
)
157163
}

0 commit comments

Comments
 (0)