Skip to content

Commit

Permalink
Scalafmt: move line ending choice to FormatWriter
Browse files Browse the repository at this point in the history
Now that the parser supports both line ending types in input directly,
we can generate the right output rather than do a post-conversion.
  • Loading branch information
kitbellew committed Jul 9, 2024
1 parent 2b580df commit add8613
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 41 deletions.
4 changes: 1 addition & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -5319,9 +5319,7 @@ takes the following values:
- `windows`: uses CRLF (`U+000D U+000A`)
- `preserve`: if an input file _contains_ CRLF anywhere, use CRLF for every line; otherwise, LF

```scala mdoc:defaults
lineEndings
```
By default, this parameter is assumed to be set to `unix`.

### `rewriteTokens`

Expand Down
23 changes: 1 addition & 22 deletions scalafmt-core/shared/src/main/scala/org/scalafmt/Scalafmt.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package org.scalafmt

import org.scalafmt.Error.PreciseIncomplete
import org.scalafmt.config.FormatEvent.CreateFormatOps
import org.scalafmt.config.LineEndings
import org.scalafmt.config.NamedDialect
import org.scalafmt.config.ScalafmtConfig
import org.scalafmt.internal.BestFirstSearch
Expand Down Expand Up @@ -30,8 +29,6 @@ import metaconfig.Configured
*/
object Scalafmt {

private val WinLineEnding = "\r\n"
private val UnixLineEnding = "\n"
private val defaultFilename = "<input>"

// XXX: don't modify signature, scalafmt-dynamic expects it via reflection
Expand Down Expand Up @@ -78,28 +75,10 @@ object Scalafmt {
else baseStyle.getConfigFor(filename).map(getStyleByFile)
styleTry.fold(
Formatted.Result(_, baseStyle),
formatCodeWithStyle(code, _, range, filename),
x => Formatted.Result(doFormat(code, x, filename, range), x),
)
}

private def formatCodeWithStyle(
code: String,
style: ScalafmtConfig,
range: Set[Range],
filename: String,
): Formatted.Result = {
val isWin = code.contains(WinLineEnding)
val unixCode =
if (isWin) code.replaceAll(WinLineEnding, UnixLineEnding) else code
val res = doFormat(unixCode, style, filename, range).map { x =>
val s = if (x.isEmpty) UnixLineEnding else x
val asWin = style.lineEndings == LineEndings.windows ||
(isWin && style.lineEndings == LineEndings.preserve)
if (asWin) s.replaceAll(UnixLineEnding, WinLineEnding) else s
}
Formatted.Result(res, style)
}

private def doFormat(
code: String,
style: ScalafmtConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ case class ScalafmtConfig(
align: Align = Align(),
spaces: Spaces = Spaces(),
literals: Literals = Literals(),
lineEndings: LineEndings = LineEndings.unix,
lineEndings: Option[LineEndings] = None,
rewriteTokens: Map[String, String] = Map.empty[String, String],
rewrite: RewriteSettings = RewriteSettings.default,
indentOperator: IndentOperator = IndentOperator(),
Expand Down Expand Up @@ -175,6 +175,9 @@ case class ScalafmtConfig(
NamedDialect.getName(dialect).getOrElse("unknown dialect"),
)

def withLineEndings(value: LineEndings): ScalafmtConfig =
copy(lineEndings = Option(value))

private lazy val forMain: ScalafmtConfig =
if (project.layout.isEmpty) forTest
else rewrite.forMainOpt.fold(this)(x => copy(rewrite = x))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ case class FormatToken(left: Token, right: Token, meta: FormatToken.Meta) {
@inline
def hasBreakOrEOF: Boolean = hasBreak || right.is[Token.EOF]

def hasCRLF: Boolean = between.exists {
case _: Token.CRLF => true
case t: Token.MultiNL => t.tokens.exists(_.is[Token.CRLF])
case _ => false
}

/** A format token is uniquely identified by its left token.
*/
override def hashCode(): Int = hash(left).##
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.scalafmt.Scalafmt
import org.scalafmt.config.Comments
import org.scalafmt.config.Docstrings
import org.scalafmt.config.FormatEvent
import org.scalafmt.config.LineEndings
import org.scalafmt.config.Newlines
import org.scalafmt.config.RewriteScala3Settings
import org.scalafmt.config.ScalafmtConfig
Expand Down Expand Up @@ -106,12 +107,19 @@ class FormatWriter(formatOps: FormatOps) {
val depth = state.depth
require(toks.length >= depth, "splits !=")
val result = new Array[FormatLocation](depth)
// 1 yes, 0 tbd, -1 no
var useCRLF = initStyle.lineEndings.fold(-1) {
case LineEndings.unix => -1
case LineEndings.windows => 1
case LineEndings.preserve => 0
}

@tailrec
def iter(cur: State, lineId: Int, gapId: Int): Unit = {
val prev = cur.prev
val idx = prev.depth
val ft = toks(idx)
if (useCRLF == 0 && ft.hasCRLF) useCRLF = 1
if (idx == 0) // done
result(idx) = FormatLocation(ft, cur, initStyle, lineId, gapId)
else {
Expand Down Expand Up @@ -139,7 +147,7 @@ class FormatWriter(formatOps: FormatOps) {
) replaceRedundantBraces(result)
}

new FormatLocations(result, "\n")
new FormatLocations(result, if (useCRLF > 0) "\r\n" else "\n")
}

private def replaceRedundantBraces(locations: Array[FormatLocation]): Unit = {
Expand Down
35 changes: 27 additions & 8 deletions scalafmt-tests/src/test/scala/org/scalafmt/EmptyFileTest.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.scalafmt

import org.scalafmt.config.LineEndings
import org.scalafmt.config.ScalafmtConfig

import scala.meta.internal.prettyprinters.DoubleQuotes

import java.lang.System.lineSeparator
Expand All @@ -8,16 +11,32 @@ import munit.FunSuite

class EmptyFileTest extends FunSuite {

private val cfgWithLineEndingsCRLF = ScalafmtConfig.default
.withLineEndings(LineEndings.windows)
private val cfgWithLineEndingsKeep = ScalafmtConfig.default
.withLineEndings(LineEndings.preserve)

Seq(
("", "\n"),
(" \n \n ", "\n"),
(" \r\n \r\n ", "\n"),
(lineSeparator(), "\n"),
(s" $lineSeparator ", "\n"),
).foreach { case (original, expected) =>
("", "\n", "\r\n", "\n"),
(" \n \n ", "\n", "\r\n", "\n"),
(" \r\n \r\n ", "\n", "\r\n", "\r\n"),
(lineSeparator(), "\n", "\r\n", lineSeparator()),
(s" $lineSeparator ", "\n", "\r\n", lineSeparator()),
).foreach { case (original, expectedUnix, expectedCrlf, expectedKeep) =>
defineTest(original, expectedUnix, ScalafmtConfig.default, "unix")
defineTest(original, expectedCrlf, cfgWithLineEndingsCRLF, "crlf")
defineTest(original, expectedKeep, cfgWithLineEndingsKeep, "keep")
}

private def defineTest(
original: String,
expected: String,
cfg: ScalafmtConfig,
label: String,
): Unit = {
val expectedQuoted = DoubleQuotes(expected)
test(s"empty tree formats to newline: ${DoubleQuotes(original)} -> $expectedQuoted") {
val obtained = Scalafmt.format(original).get
test(s"empty tree formats to newline [$label]: ${DoubleQuotes(original)} -> $expectedQuoted") {
val obtained = Scalafmt.format(original, cfg).get
if (obtained != expected) fail(
s"values are not equal: ${DoubleQuotes(obtained)} != $expectedQuoted",
)
Expand Down
12 changes: 6 additions & 6 deletions scalafmt-tests/src/test/scala/org/scalafmt/LineEndingsTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,47 +11,47 @@ class LineEndingsTest extends FunSuite {
val original = "@ Singleton\r\nobject a {\r\nval y = 2\r\n}"
val expected = "@Singleton\r\nobject a {\r\n val y = 2\r\n}\r\n"
val obtained = Scalafmt
.format(original, ScalafmtConfig.default.copy(lineEndings = preserve)).get
.format(original, ScalafmtConfig.default.withLineEndings(preserve)).get
assertNoDiff(obtained, expected)
}

test("code with unix line endings after formatting with line endings preserve setting should have the same endings") {
val original = "@ Singleton\nobject a {\nval y = 2\n}"
val expected = "@Singleton\nobject a {\n val y = 2\n}\n"
val obtained = Scalafmt
.format(original, ScalafmtConfig.default.copy(lineEndings = preserve)).get
.format(original, ScalafmtConfig.default.withLineEndings(preserve)).get
assertNoDiff(obtained, expected)
}

test("code with windows line endings after formatting with line endings windows setting should have windows endings") {
val original = "@ Singleton\r\nobject a {\r\nval y = 2\r\n}"
val expected = "@Singleton\r\nobject a {\r\n val y = 2\r\n}\r\n"
val obtained = Scalafmt
.format(original, ScalafmtConfig.default.copy(lineEndings = windows)).get
.format(original, ScalafmtConfig.default.withLineEndings(windows)).get
assertNoDiff(obtained, expected)
}

test("code with unix line endings after formatting with line endings windows setting should have windows endings") {
val original = "@ Singleton\nobject a {\nval y = 2\n}"
val expected = "@Singleton\r\nobject a {\r\n val y = 2\r\n}\r\n"
val obtained = Scalafmt
.format(original, ScalafmtConfig.default.copy(lineEndings = windows)).get
.format(original, ScalafmtConfig.default.withLineEndings(windows)).get
assertNoDiff(obtained, expected)
}

test("code with windows line endings after formatting with line endings unix setting should have unix endings") {
val original = "@ Singleton\r\nobject a {\r\nval y = 2\r\n}"
val expected = "@Singleton\nobject a {\n val y = 2\n}\n"
val obtained = Scalafmt
.format(original, ScalafmtConfig.default.copy(lineEndings = unix)).get
.format(original, ScalafmtConfig.default.withLineEndings(unix)).get
assertNoDiff(obtained, expected)
}

test("code with unix line endings after formatting with line endings unix setting should have unix endings") {
val original = "@ Singleton\nobject a {\nval y = 2\n}"
val expected = "@Singleton\nobject a {\n val y = 2\n}\n"
val obtained = Scalafmt
.format(original, ScalafmtConfig.default.copy(lineEndings = unix)).get
.format(original, ScalafmtConfig.default.withLineEndings(unix)).get
assertNoDiff(obtained, expected)
}
}

0 comments on commit add8613

Please sign in to comment.