diff --git a/core/pom.xml b/core/pom.xml index 267689e1..76297494 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -137,6 +137,15 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + -Dfile.encoding=UTF-16 + + diff --git a/core/src/main/java/com/facebook/ktfmt/cli/Main.kt b/core/src/main/java/com/facebook/ktfmt/cli/Main.kt index 980897de..5f46eb92 100644 --- a/core/src/main/java/com/facebook/ktfmt/cli/Main.kt +++ b/core/src/main/java/com/facebook/ktfmt/cli/Main.kt @@ -20,11 +20,15 @@ import com.facebook.ktfmt.format.Formatter import com.facebook.ktfmt.format.ParseError import com.google.googlejavaformat.FormattingError import java.io.BufferedReader +import java.io.BufferedWriter import java.io.File +import java.io.FileInputStream import java.io.IOException import java.io.InputStream import java.io.InputStreamReader +import java.io.OutputStreamWriter import java.io.PrintStream +import java.nio.charset.StandardCharsets.UTF_8 import java.util.concurrent.atomic.AtomicInteger import kotlin.system.exitProcess @@ -125,7 +129,8 @@ class Main( private fun format(file: File?): Boolean { val fileName = file?.toString() ?: parsedArgs.stdinName ?: "" try { - val code = file?.readText() ?: BufferedReader(InputStreamReader(input)).readText() + val bytes = if (file == null) input else FileInputStream(file) + val code = BufferedReader(InputStreamReader(bytes, UTF_8)).readText() val formattedCode = Formatter.format(parsedArgs.formattingOptions, code) val alreadyFormatted = code == formattedCode @@ -136,7 +141,7 @@ class Main( out.println(fileName) } } else { - out.print(formattedCode) + BufferedWriter(OutputStreamWriter(out, UTF_8)).use { it.write(formattedCode) } } return alreadyFormatted } @@ -148,7 +153,7 @@ class Main( } else { // TODO(T111284144): Add tests if (!alreadyFormatted) { - file.writeText(formattedCode) + file.writeText(formattedCode, UTF_8) } err.println("Done formatting $fileName") } @@ -158,18 +163,14 @@ class Main( err.println("Error formatting $fileName: ${e.message}; skipping.") throw e } catch (e: ParseError) { - handleParseError(fileName, e) + err.println("$fileName:${e.message}") throw e } catch (e: FormattingError) { for (diagnostic in e.diagnostics()) { - System.err.println("$fileName:$diagnostic") + err.println("$fileName:$diagnostic") } e.printStackTrace(err) throw e } } - - private fun handleParseError(fileName: String, e: ParseError) { - err.println("$fileName:${e.message}") - } } diff --git a/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt b/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt index 2c849722..e0009f7b 100644 --- a/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt +++ b/core/src/main/java/com/facebook/ktfmt/cli/ParsedArgs.kt @@ -20,6 +20,7 @@ import com.facebook.ktfmt.format.Formatter import com.facebook.ktfmt.format.FormattingOptions import java.io.File import java.io.PrintStream +import java.nio.charset.StandardCharsets.UTF_8 /** ParsedArgs holds the arguments passed to ktfmt on the command-line, after parsing. */ data class ParsedArgs( @@ -40,7 +41,7 @@ data class ParsedArgs( fun processArgs(err: PrintStream, args: Array): ParsedArgs { if (args.size == 1 && args[0].startsWith("@")) { - return parseOptions(err, File(args[0].substring(1)).readLines().toTypedArray()) + return parseOptions(err, File(args[0].substring(1)).readLines(UTF_8).toTypedArray()) } else { return parseOptions(err, args) } diff --git a/core/src/test/java/com/facebook/ktfmt/cli/MainTest.kt b/core/src/test/java/com/facebook/ktfmt/cli/MainTest.kt index 3697d72b..6e207fe7 100644 --- a/core/src/test/java/com/facebook/ktfmt/cli/MainTest.kt +++ b/core/src/test/java/com/facebook/ktfmt/cli/MainTest.kt @@ -21,11 +21,15 @@ import java.io.ByteArrayOutputStream import java.io.File import java.io.PrintStream import java.lang.IllegalStateException +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.nio.charset.StandardCharsets.UTF_8 import java.nio.file.Files import java.util.concurrent.ForkJoinPool import kotlin.io.path.createTempDirectory import org.junit.After import org.junit.Assert.fail +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -40,6 +44,13 @@ class MainTest { private val out = ByteArrayOutputStream() private val err = ByteArrayOutputStream() + private val testCharset = StandardCharsets.UTF_16 + + @Before + fun setUp() { + assertThat(Charset.defaultCharset()).isEqualTo(testCharset) // Verify the test JVM flags + } + @After fun tearDown() { root.deleteRecursively() @@ -52,7 +63,7 @@ class MainTest { @Test fun `expandArgsToFileNames - single file arg is used as is`() { val fooBar = root.resolve("foo.bar") - fooBar.writeText("hi") + fooBar.writeText("hi", UTF_8) assertThat(Main.expandArgsToFileNames(listOf(fooBar.toString()))).containsExactly(fooBar) } @@ -67,9 +78,9 @@ class MainTest { val dir = root.resolve("dir") dir.mkdirs() val foo = dir.resolve("foo.kt") - foo.writeText("") + foo.writeText("", UTF_8) val bar = dir.resolve("bar.kt") - bar.writeText("") + bar.writeText("", UTF_8) assertThat(Main.expandArgsToFileNames(listOf(dir.toString()))).containsExactly(foo, bar) } @@ -78,16 +89,16 @@ class MainTest { val dir1 = root.resolve("dir1") dir1.mkdirs() val foo1 = dir1.resolve("foo1.kt") - foo1.writeText("") + foo1.writeText("", UTF_8) val bar1 = dir1.resolve("bar1.kt") - bar1.writeText("") + bar1.writeText("", UTF_8) val dir2 = root.resolve("dir2") dir1.mkdirs() val foo2 = dir1.resolve("foo2.kt") - foo2.writeText("") + foo2.writeText("", UTF_8) val bar2 = dir1.resolve("bar2.kt") - bar2.writeText("") + bar2.writeText("", UTF_8) assertThat(Main.expandArgsToFileNames(listOf(dir1.toString(), dir2.toString()))) .containsExactly(foo1, bar1, foo2, bar2) @@ -109,7 +120,7 @@ class MainTest { Main(code.byteInputStream(), PrintStream(out), PrintStream(err), arrayOf("-")).run() val expected = "fun f1(): Int = 0\n" - assertThat(out.toString("UTF-8")).isEqualTo(expected) + assertThat(out.toString(UTF_8)).isEqualTo(expected) } @Test @@ -119,7 +130,7 @@ class MainTest { Main(code.byteInputStream(), PrintStream(out), PrintStream(err), arrayOf("-")).run() assertThat(returnValue).isEqualTo(1) - assertThat(err.toString("UTF-8")).startsWith(":1:14: error: ") + assertThat(err.toString(testCharset)).startsWith(":1:14: error: ") } @Test @@ -134,18 +145,18 @@ class MainTest { .run() assertThat(returnValue).isEqualTo(1) - assertThat(err.toString("UTF-8")).startsWith("file/Foo.kt:1:14: error: ") + assertThat(err.toString(testCharset)).startsWith("file/Foo.kt:1:14: error: ") } @Test fun `Parsing errors are reported (file)`() { val fooBar = root.resolve("foo.kt") - fooBar.writeText("fun f1 ( ") + fooBar.writeText("fun f1 ( ", UTF_8) val returnValue = Main(emptyInput, PrintStream(out), PrintStream(err), arrayOf(fooBar.toString())).run() assertThat(returnValue).isEqualTo(1) - assertThat(err.toString("UTF-8")).contains("foo.kt:1:14: error: ") + assertThat(err.toString(testCharset)).contains("foo.kt:1:14: error: ") } @Test @@ -153,9 +164,9 @@ class MainTest { val file1 = root.resolve("file1.kt") val file2Broken = root.resolve("file2.kt") val file3 = root.resolve("file3.kt") - file1.writeText("fun f1 () ") - file2Broken.writeText("fun f1 ( ") - file3.writeText("fun f1 () ") + file1.writeText("fun f1 () ", UTF_8) + file2Broken.writeText("fun f1 ( ", UTF_8) + file3.writeText("fun f1 () ", UTF_8) // Make Main() process files serially. val forkJoinPool = ForkJoinPool(1) @@ -173,16 +184,16 @@ class MainTest { .get() assertThat(returnValue).isEqualTo(1) - assertThat(err.toString("UTF-8")).contains("Done formatting $file1") - assertThat(err.toString("UTF-8")).contains("file2.kt:1:14: error: ") - assertThat(err.toString("UTF-8")).contains("Done formatting $file3") + assertThat(err.toString(testCharset)).contains("Done formatting $file1") + assertThat(err.toString(testCharset)).contains("file2.kt:1:14: error: ") + assertThat(err.toString(testCharset)).contains("Done formatting $file3") } @Test fun `file is not modified if it is already formatted`() { val code = """fun f() = println("hello, world")""" + "\n" val formattedFile = root.resolve("formatted_file.kt") - formattedFile.writeText(code) + formattedFile.writeText(code, UTF_8) val formattedFilePath = formattedFile.toPath() val lastModifiedTimeBeforeRunningFormatter = @@ -199,7 +210,7 @@ class MainTest { fun `file is modified if it is not formatted`() { val code = """fun f() = println( "hello, world")""" + "\n" val unformattedFile = root.resolve("unformatted_file.kt") - unformattedFile.writeText(code) + unformattedFile.writeText(code, UTF_8) val unformattedFilePath = unformattedFile.toPath() val lastModifiedTimeBeforeRunningFormatter = @@ -225,7 +236,7 @@ class MainTest { } """ val fooBar = root.resolve("foo.kt") - fooBar.writeText(code) + fooBar.writeText(code, UTF_8) Main( emptyInput, @@ -264,7 +275,7 @@ class MainTest { arrayOf("--dropbox-style", "-")) .run() - assertThat(out.toString("UTF-8")).isEqualTo(formatted) + assertThat(out.toString(UTF_8)).isEqualTo(formatted) } @Test @@ -298,7 +309,7 @@ class MainTest { PrintStream(err), arrayOf("-")) .run() - assertThat(out.toString("UTF-8")).isEqualTo(expected) + assertThat(out.toString(UTF_8)).isEqualTo(expected) out.reset() @@ -308,20 +319,20 @@ class MainTest { PrintStream(err), arrayOf("-")) .run() - assertThat(out.toString("UTF-8")).isEqualTo(expected) + assertThat(out.toString(UTF_8)).isEqualTo(expected) } @Test fun `--dry-run prints filename and does not change file`() { val code = """fun f () = println( "hello, world" )""" val file = root.resolve("foo.kt") - file.writeText(code) + file.writeText(code, UTF_8) Main(emptyInput, PrintStream(out), PrintStream(err), arrayOf("--dry-run", file.toString())) .run() assertThat(file.readText()).isEqualTo(code) - assertThat(out.toString("UTF-8")).contains(file.toString()) + assertThat(out.toString(testCharset)).contains(file.toString()) } @Test @@ -331,20 +342,20 @@ class MainTest { Main(code.byteInputStream(), PrintStream(out), PrintStream(err), arrayOf("--dry-run", "-")) .run() - assertThat(out.toString("UTF-8")).doesNotContain("hello, world") - assertThat(out.toString("UTF-8")).isEqualTo("\n") + assertThat(out.toString(UTF_8)).doesNotContain("hello, world") + assertThat(out.toString(testCharset)).isEqualTo("\n") } @Test fun `--dry-run prints nothing when there are no changes needed (file)`() { val code = """fun f() = println("hello, world")\n""" val file = root.resolve("foo.kt") - file.writeText(code) + file.writeText(code, UTF_8) Main(emptyInput, PrintStream(out), PrintStream(err), arrayOf("--dry-run", file.toString())) .run() - assertThat(out.toString("UTF-8")).isEmpty() + assertThat(out.toString(UTF_8)).isEmpty() } @Test @@ -354,14 +365,14 @@ class MainTest { Main(code.byteInputStream(), PrintStream(out), PrintStream(err), arrayOf("--dry-run", "-")) .run() - assertThat(out.toString("UTF-8")).isEmpty() + assertThat(out.toString(UTF_8)).isEmpty() } @Test fun `Exit code is 0 when there are changes (file)`() { val code = """fun f () = println( "hello, world" )""" val file = root.resolve("foo.kt") - file.writeText(code) + file.writeText(code, UTF_8) val exitCode = Main(emptyInput, PrintStream(out), PrintStream(err), arrayOf(file.toString())).run() @@ -383,7 +394,7 @@ class MainTest { fun `Exit code is 1 when there are changes and --set-exit-if-changed is set (file)`() { val code = """fun f () = println( "hello, world" )""" val file = root.resolve("foo.kt") - file.writeText(code) + file.writeText(code, UTF_8) val exitCode = Main( @@ -415,7 +426,7 @@ class MainTest { fun `--set-exit-if-changed and --dry-run changes nothing, prints filenames, and exits with 1 (file)`() { val code = """fun f () = println( "hello, world" )""" val file = root.resolve("foo.kt") - file.writeText(code) + file.writeText(code, UTF_8) val exitCode = Main( @@ -426,7 +437,7 @@ class MainTest { .run() assertThat(file.readText()).isEqualTo(code) - assertThat(out.toString("UTF-8")).contains(file.toString()) + assertThat(out.toString(testCharset)).contains(file.toString()) assertThat(exitCode).isEqualTo(1) } @@ -442,8 +453,8 @@ class MainTest { arrayOf("--dry-run", "--set-exit-if-changed", "-")) .run() - assertThat(out.toString("UTF-8")).doesNotContain("hello, world") - assertThat(out.toString("UTF-8")).isEqualTo("\n") + assertThat(out.toString(UTF_8)).doesNotContain("hello, world") + assertThat(out.toString(testCharset)).isEqualTo("\n") assertThat(exitCode).isEqualTo(1) } @@ -451,7 +462,7 @@ class MainTest { fun `--stdin-name can only be used with stdin`() { val code = """fun f () = println( "hello, world" )""" val file = root.resolve("foo.kt") - file.writeText(code) + file.writeText(code, UTF_8) val exitCode = Main( @@ -462,8 +473,46 @@ class MainTest { .run() assertThat(file.readText()).isEqualTo(code) - assertThat(out.toString("UTF-8")).isEmpty() - assertThat(err.toString("UTF-8")).isEqualTo("Error: --stdin-name can only be used with stdin\n") + assertThat(out.toString(UTF_8)).isEmpty() + assertThat(err.toString(testCharset)) + .isEqualTo("Error: --stdin-name can only be used with stdin\n") assertThat(exitCode).isEqualTo(1) } + + @Test + fun `Always use UTF8 encoding (stdin, stdout)`() { + val code = """fun f () = println( "hello, world" )""" + val expected = """fun f() = println("hello, world")""" + "\n" + + val exitCode = + Main( + code.byteInputStream(UTF_8), + PrintStream(out, true, testCharset), + PrintStream(err), + arrayOf("-"), + ) + .run() + + assertThat(exitCode).isEqualTo(0) + assertThat(out.toString(UTF_8)).isEqualTo(expected) + } + + @Test + fun `Always use UTF8 encoding (file)`() { + val code = """fun f() = println( "hello, world")""" + "\n" + val file = root.resolve("unformatted_file.kt") + file.writeText(code, UTF_8) + + val exitCode = + Main( + emptyInput, + PrintStream(out), + PrintStream(err), + arrayOf(file.toString()), + ) + .run() + + assertThat(exitCode).isEqualTo(0) + assertThat(file.readText(UTF_8)).isEqualTo("""fun f() = println("hello, world")""" + "\n") + } }