Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies {
api(libs.kotlin.stdlib)
api(libs.kotlin.test)
api(libs.kotlin.compilerEmbeddable)
implementation(libs.ec4j)
testImplementation(libs.googleTruth)
testImplementation(libs.junit)
}
Expand Down
95 changes: 95 additions & 0 deletions core/src/main/java/com/facebook/ktfmt/cli/EditorConfigResolver.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.facebook.ktfmt.cli

import com.facebook.ktfmt.format.FormattingOptions
import com.facebook.ktfmt.format.TrailingCommaManagementStrategy
import java.io.File
import java.util.concurrent.ConcurrentHashMap
import org.ec4j.core.EditorConfigLoader
import org.ec4j.core.PropertyTypeRegistry
import org.ec4j.core.Resource
import org.ec4j.core.ResourcePropertiesService
import org.ec4j.core.model.EditorConfig
import org.ec4j.core.model.PropertyType
import org.ec4j.core.model.Version.CURRENT

object EditorConfigResolver {

private val ijContinuationIndentSize: PropertyType<Int> =
PropertyType.LowerCasingPropertyType(
"ij_continuation_indent_size",
"Denotes the continuation indent size. Useful to distinguish code blocks versus continuation lines",
PropertyType.PropertyValueParser.POSITIVE_INT_VALUE_PARSER,
)

private val commaManagementStrategy: PropertyType<TrailingCommaManagementStrategy> =
PropertyType.LowerCasingPropertyType(
"ktfmt_trailing_comma_management_strategy",
"Ktfmt Trailing Comma Management Strategy",
{ _: String, value: String ->
TrailingCommaManagementStrategy.entries
.find { it.name.lowercase() == value }
?.let { PropertyType.PropertyValue.valid(value, it) }
?: PropertyType.PropertyValue.invalid(
value,
"Unknown ktfmt_trailing_comma_management_strategy value '$value'",
)
},
TrailingCommaManagementStrategy.entries.map { it.name.lowercase() }.toSet(),
)
private val propertyTypeRegistry by lazy {
PropertyTypeRegistry.builder()
.defaults()
.type(PropertyType.max_line_length) // missing from defaults?
.type(ijContinuationIndentSize)
.type(commaManagementStrategy)
.build()
}

private object Cache : org.ec4j.core.Cache {
val cached = ConcurrentHashMap<Resource, EditorConfig>()

override fun get(
editorConfigFile: Resource,
loader: EditorConfigLoader,
): EditorConfig = cached.computeIfAbsent(editorConfigFile, loader::load)
}

private val resourcePropertiesService: ResourcePropertiesService by lazy {
ResourcePropertiesService.builder()
.cache(Cache)
.loader(EditorConfigLoader.of(CURRENT, propertyTypeRegistry))
.build()
}

fun resolveFormattingOptions(file: File, baseOptions: FormattingOptions): FormattingOptions {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add unit test(s) to this method?

val props =
resourcePropertiesService.queryProperties(
Resource.Resources.ofPath(file.toPath(), Charsets.UTF_8)
)

// `max_line_length` may return null to indicate 'off', in which case we keep the base maxWidth
val maxWidth =
props.getValue(PropertyType.max_line_length, baseOptions.maxWidth, false)
?: baseOptions.maxWidth

// `indent_size` may return null to indicate 'tab', in which case we defer to `tab_width`
val blockIndent =
props.getValue(PropertyType.indent_size, baseOptions.blockIndent, false)
?: props.getValue(PropertyType.tab_width, baseOptions.blockIndent, false)

val continuationIndent =
props.getValue(ijContinuationIndentSize, baseOptions.continuationIndent, false)

val trailingCommaStrategy =
props.getValue(commaManagementStrategy, baseOptions.trailingCommaManagementStrategy, false)

val resolved =
baseOptions.copy(
maxWidth = maxWidth,
blockIndent = blockIndent,
continuationIndent = continuationIndent,
trailingCommaManagementStrategy = trailingCommaStrategy,
)
return resolved
}
}
5 changes: 4 additions & 1 deletion core/src/main/java/com/facebook/ktfmt/cli/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,12 @@ class Main(
private fun format(file: File?, args: ParsedArgs): Boolean {
val fileName = file?.toString() ?: args.stdinName ?: "<stdin>"
try {
val formattingOptions =
if (file == null || !args.editorConfig) args.formattingOptions
else EditorConfigResolver.resolveFormattingOptions(file, args.formattingOptions)
val bytes = if (file == null) input else FileInputStream(file)
val code = BufferedReader(InputStreamReader(bytes, UTF_8)).readText()
val formattedCode = Formatter.format(args.formattingOptions, code)
val formattedCode = Formatter.format(formattingOptions, code)
val alreadyFormatted = code == formattedCode

// stdin
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ data class ParsedArgs(
val setExitIfChanged: Boolean,
/** File name to report when formating code from stdin */
val stdinName: String?,
val editorConfig: Boolean,
) {
companion object {

Expand Down Expand Up @@ -81,6 +82,7 @@ data class ParsedArgs(
| --set-exit-if-changed Sets exit code to 1 if any input file was not
| formatted/touched
| --do-not-remove-unused-imports Leaves all imports in place, even if not used
| --enable-editorconfig Enable .editorconfig overrides for supported formatting options (limited).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's document what is this (limited) somewhere that we can link it.
It could be the README.md

|
|ARGFILE:
| If the only argument begins with '@', the remainder of the argument is treated
Expand All @@ -106,6 +108,7 @@ data class ParsedArgs(
var setExitIfChanged = false
var removeUnusedImports = true
var stdinName: String? = null
var editorConfig = false

if ("--help" in args || "-h" in args) return ParseResult.ShowMessage(HELP_TEXT)
if ("--version" in args || "-v" in args) {
Expand All @@ -120,6 +123,7 @@ data class ParsedArgs(
arg == "--dry-run" || arg == "-n" -> dryRun = true
arg == "--set-exit-if-changed" -> setExitIfChanged = true
arg == "--do-not-remove-unused-imports" -> removeUnusedImports = false
arg == "--enable-editorconfig" -> editorConfig = true
arg.startsWith("--stdin-name=") ->
stdinName =
parseKeyValueArg("--stdin-name", arg)
Expand Down Expand Up @@ -152,6 +156,7 @@ data class ParsedArgs(
dryRun,
setExitIfChanged,
stdinName,
editorConfig,
)
)
}
Expand Down
16 changes: 15 additions & 1 deletion core/src/test/java/com/facebook/ktfmt/cli/ParsedArgsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ class ParsedArgsTest {
assertThat(parsed.formattingOptions.removeUnusedImports).isFalse()
}

@Test
fun `parseOptions recognizes --enable-editorconfig`() {
val parsed = assertSucceeds(ParsedArgs.parseOptions(arrayOf("--enable-editorconfig", "foo.kt")))
assertThat(parsed.editorConfig).isEqualTo(true)
}

@Test
fun `parseOptions recognizes --stdin-name`() {
val parsed = assertSucceeds(ParsedArgs.parseOptions(arrayOf("--stdin-name=my/foo.kt", "-")))
Expand Down Expand Up @@ -244,11 +250,19 @@ class ParsedArgsTest {
setExitIfChanged: Boolean = false,
removedUnusedImports: Boolean = true,
stdinName: String? = null,
editorConfig: Boolean = false,
): ParseResult.Ok {
val returnedFormattingOptions =
formattingOptions.copy(removeUnusedImports = removedUnusedImports)
return ParseResult.Ok(
ParsedArgs(fileNames, returnedFormattingOptions, dryRun, setExitIfChanged, stdinName)
ParsedArgs(
fileNames,
returnedFormattingOptions,
dryRun,
setExitIfChanged,
stdinName,
editorConfig,
)
)
}
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ amazon-aws-lambda-core = { module = "com.amazonaws:aws-lambda-java-core", versio
amazon-aws-lambda-events = { module = "com.amazonaws:aws-lambda-java-events", version.ref = "com-amazonaws-aws-lambda-java-events" }
amazon-aws-sdk-bom = { module = "software.amazon.awssdk:bom", version = "2.10.73" }
amazon-aws-sdk-lambda = { module = "software.amazon.awssdk:lambda" }
ec4j = { module = "org.ec4j.core:ec4j-core", version = "1.1.1" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use the latest stable 1.2.0

googleJavaformat = { module = "com.google.googlejavaformat:google-java-format", version.ref = "com-google-googlejavaformat-google-java-format" }
googleTruth = { module = "com.google.truth:truth", version.ref = "com-google-truth-truth" }
gson = { module = "com.google.code.gson:gson", version.ref = "com-google-code-gson-gson" }
Expand Down