Skip to content

Commit

Permalink
FormatWriter: add braces if span exceeds threshold
Browse files Browse the repository at this point in the history
  • Loading branch information
kitbellew committed Dec 11, 2021
1 parent e60d1fb commit 5935475
Show file tree
Hide file tree
Showing 10 changed files with 917 additions and 367 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2527,6 +2527,167 @@ class FormatOps(

}

object MissingBraces {

type Ranges = Seq[(Tree, Tree)]
type Result = Option[(Tree, Ranges)]

private trait Factory {
def getBlocks(ft: FormatToken, nft: FormatToken, all: Boolean): Result
}

def getBlocks(ft: FormatToken, all: Boolean): Result = {
val nft = nextNonComment(ft)
if (nft.right.is[T.LeftBrace]) None
else {
val impl = ft.left match {
case _: T.RightArrow => RightArrowImpl
case _: T.RightParen => RightParenImpl
case _: T.RightBrace => RightBraceImpl
case _: T.KwDo => DoImpl
case _: T.Equals => EqualsImpl
case _: T.KwTry => TryImpl
case _: T.KwCatch => CatchImpl
case _: T.KwFinally => FinallyImpl
case _: T.KwElse => ElseImpl
case _: T.KwYield => YieldImpl
case _ => null
}
Option(impl).flatMap(_.getBlocks(ft, nft, all))
}
}

private def seq(all: Boolean, t: Tree): Ranges =
if (all) Seq(t -> t) else Nil

private def seq(all: Boolean, t: Option[Tree]): Ranges =
t.map(seq(all, _)).getOrElse(Nil)

private def seq(all: Boolean, t: Seq[Tree]): Ranges =
if (all && t.nonEmpty) Seq(t.head -> t.last) else Nil

private def seq(all: Boolean, t: Tree, ts: Seq[Tree]): Ranges =
if (all) Seq(t -> ts.lastOption.getOrElse(t)) else Nil

private object BlockImpl extends Factory {
def getBlocks(ft: FormatToken, nft: FormatToken, all: Boolean): Result = {
def ok(stat: Tree): Boolean = stat.tokens.headOption.contains(nft.right)
val leftOwner = ft.meta.leftOwner
findTreeWithParentSimple(nft.meta.rightOwner)(_ eq leftOwner) match {
case Some(t: Term.Block) =>
if (t.stats.headOption.exists(ok)) Some((t, Nil)) else None
case x => x.filter(ok).map((_, Nil))
}
}
}

private object RightArrowImpl extends Factory {
def getBlocks(ft: FormatToken, nft: FormatToken, all: Boolean): Result =
ft.meta.leftOwner match {
case t @ Term.Function(p, b) =>
val skip = t.parent.exists(_.is[Term.Block])
if (skip) None else Some((b, seq(all, p)))
case _ => None
}
}

private object RightParenImpl extends Factory {
def getBlocks(ft: FormatToken, nft: FormatToken, all: Boolean): Result =
ft.meta.leftOwner match {
case x @ Term.If(c, t, e) if !nft.right.is[T.KwThen] =>
Some((t, seq(all && !ifWithoutElse(x), e) ++ seq(all, c)))
case Term.For(s, b) if !nft.right.is[T.KwDo] =>
Some(b, seq(all, s))
case Term.While(c, b) if !nft.right.is[T.KwDo] =>
Some(b, seq(all, c))
case _ => None
}
}

private object RightBraceImpl extends Factory {
def getBlocks(ft: FormatToken, nft: FormatToken, all: Boolean): Result =
ft.meta.leftOwner match {
case t @ Term.For(s, b)
if !nft.right.is[T.KwDo] && !isLastToken(ft.left, t) =>
Some(b, seq(all, s))
case _ => None
}
}

private object DoImpl extends Factory {
def getBlocks(ft: FormatToken, nft: FormatToken, all: Boolean): Result =
ft.meta.leftOwner match {
case Term.Do(b, c) => Some(b, seq(all, c))
case _ => None
}
}

private object EqualsImpl extends Factory {
def getBlocks(ft: FormatToken, nft: FormatToken, all: Boolean): Result =
ft.meta.leftOwner match {
case t: Ctor.Secondary => Some(t, seq(all, t.init, t.stats))
case t: Defn.Def => Some(t.body, Nil)
case t: Defn.Macro => Some(t.body, Nil)
case t: Term.Assign => Some(t.rhs, Nil)
case t: Defn.Type => Some(t.body, Nil)
case t: Defn.Val => Some(t.rhs, Nil)
case t: Defn.Var => t.rhs.map(_ -> Nil)
case _ => BlockImpl.getBlocks(ft, nft, all)
}
}

private object TryImpl extends Factory {
def getBlocks(ft: FormatToken, nft: FormatToken, all: Boolean): Result =
ft.meta.leftOwner match {
case t: Term.Try =>
Some(t.expr, seq(all, t.catchp) ++ seq(all, t.finallyp))
case t: Term.TryWithHandler =>
Some(t.expr, seq(all, t.catchp) ++ seq(all, t.finallyp))
case _ => None
}
}

private object CatchImpl extends Factory {
def getBlocks(ft: FormatToken, nft: FormatToken, all: Boolean): Result =
ft.meta.leftOwner match {
case t: Term.Try =>
Some(t.catchp.last, seq(all, t.expr) ++ seq(all, t.finallyp))
case t: Term.TryWithHandler =>
Some(t.catchp, seq(all, t.expr) ++ seq(all, t.finallyp))
case _ => None
}
}

private object FinallyImpl extends Factory {
def getBlocks(ft: FormatToken, nft: FormatToken, all: Boolean): Result =
ft.meta.leftOwner match {
case t: Term.Try =>
t.finallyp.map(x => (x, seq(all, t.expr) ++ seq(all, t.catchp)))
case t: Term.TryWithHandler =>
t.finallyp.map(x => (x, seq(all, t.expr) ++ seq(all, t.catchp)))
case _ => None
}
}

private object ElseImpl extends Factory {
def getBlocks(ft: FormatToken, nft: FormatToken, all: Boolean): Result =
ft.meta.leftOwner match {
case Term.If(c, t, e) if !e.is[Term.If] =>
Some((e, seq(all, t) ++ seq(all, c)))
case _ => None
}
}

private object YieldImpl extends Factory {
def getBlocks(ft: FormatToken, nft: FormatToken, all: Boolean): Result =
ft.meta.leftOwner match {
case Term.ForYield(s, b) => Some((b, seq(all, s)))
case _ => None
}
}

}

def isBlockWithoutOptionalBraces(t: Term.Block): Boolean =
isSingleStatBlock(t) && (
t.tokens.head match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class FormatWriter(formatOps: FormatOps) {
val location = entry.curr
implicit val style: ScalafmtConfig = location.style
val formatToken = location.formatToken
var skipWs = false

formatToken.left match {
case _ if entry.previous.formatToken.meta.formatOff =>
Expand Down Expand Up @@ -96,7 +97,20 @@ class FormatWriter(formatOps: FormatOps) {
}
}

entry.formatWhitespace
// missing braces
if (location.missingBracesIndent.nonEmpty) {
location.missingBracesIndent.toSeq
.sorted(Ordering.Int.reverse)
.foreach(i => sb.append('\n').append(getIndentation(i)).append("}"))
if (location.missingBracesOpenOrTuck) {
skipWs = true
sb.append(" ")
} else if (formatToken.right.is[T.RightParen])
skipWs = true
} else if (location.missingBracesOpenOrTuck)
sb.append(" {")

if (!skipWs) entry.formatWhitespace
}

sb.toString()
Expand Down Expand Up @@ -129,6 +143,8 @@ class FormatWriter(formatOps: FormatOps) {
if (initStyle.rewrite.scala3.insertEndMarkerMinLines > 0)
checkInsertEndMarkers(result)
}
if (initStyle.rewrite.insertBraces.minLines > 0)
checkInsertBraces(result)
if (
initStyle.rewrite.rules.contains(RedundantBraces) &&
!initStyle.rewrite.redundantBraces.parensForOneLineApply.contains(false)
Expand Down Expand Up @@ -338,6 +354,82 @@ class FormatWriter(formatOps: FormatOps) {
}
}

private def checkInsertBraces(locations: Array[FormatLocation]): Unit = {
def checkInfix(tree: Tree): Boolean = tree match {
case ai @ Term.ApplyInfix(lhs, op, _, rhs) => {
isEnclosedInMatching(ai) ||
tokens.prevNonCommentSameLine(tokens.tokenJustBefore(op)).noBreak &&
checkInfix(lhs) && (rhs.lengthCompare(1) != 0 || checkInfix(rhs.head))
}
case _ => true
}
var addedLines = 0
val willAddLines = new ListBuffer[Int]
locations.foreach { x =>
val idx = x.formatToken.meta.idx
val floc = if (addedLines > 0 && x.isNotRemoved) {
val floc = x.copy(leftLineId = x.leftLineId - addedLines)
locations(idx) = floc
floc
} else x
if (willAddLines.nonEmpty && willAddLines(0) == idx) {
addedLines += 1
willAddLines.remove(0)
}
@tailrec
def hasBreakAfter(i: Int): Boolean = i < locations.length && {
val x = locations(i)
if (!x.isNotRemoved) hasBreakAfter(i + 1)
else if (x.hasBreakAfter) true
else if (!x.formatToken.right.is[T.Comment]) false
else hasBreakAfter(i + 1)
}
val style = floc.style
val ib = style.rewrite.insertBraces
val ft = floc.formatToken
val ok = !ft.meta.formatOff && ib.minLines > 0 &&
floc.missingBracesIndent.isEmpty
val mb =
if (ok) formatOps.MissingBraces.getBlocks(ft, ib.allBlocks).filter {
case (mb, _) => checkInfix(mb) && hasBreakAfter(idx)
}
else None
mb.foreach { case (owner, otherBlocks) =>
val endFt = tokens.nextNonCommentSameLine(tokens.getLast(owner))
val end = endFt.meta.idx
val eLoc = locations(end)
val begIndent = floc.state.prev.indentation
def checkSpan: Boolean =
getLineDiff(floc, eLoc) + addedLines >= ib.minLines ||
otherBlocks.exists { case (b, e) =>
val bIdx = tokens.tokenJustBefore(b).meta.idx
val eIdx = tokens.getLast(e).meta.idx
val span = getLineDiff(locations(bIdx), locations(eIdx))
ib.minLines <=
(if (bIdx <= idx && eIdx > idx) span + addedLines else span)
}
if (
!endFt.meta.formatOff && eLoc.hasBreakAfter &&
!eLoc.missingBracesIndent.contains(begIndent) && checkSpan
) {
val addLine = style.newlines.alwaysBeforeElseAfterCurlyIf ||
(endFt.right match {
case _: T.KwElse | _: T.KwCatch | _: T.KwFinally =>
!owner.parent.contains(endFt.meta.rightOwner)
case _ => true
})
if (addLine) willAddLines.prepend(end)
locations(idx) = floc.copy(missingBracesOpenOrTuck = true)
locations(end) = eLoc.copy(
missingBracesOpenOrTuck = !addLine &&
(eLoc.missingBracesIndent.isEmpty || eLoc.missingBracesOpenOrTuck),
missingBracesIndent = eLoc.missingBracesIndent + begIndent
)
}
}
}
}

class FormatLocations(val locations: Array[FormatLocation]) {

val tokenAligns: Map[Int, Int] = alignmentTokens
Expand Down Expand Up @@ -1494,6 +1586,9 @@ object FormatWriter {
leftLineId: Int, // counts back from the end of the file
shift: Int = 0,
optionalBraces: Map[Int, Tree] = Map.empty,
// if indent is empty, indicates open; otherwise, whether to tuck
missingBracesOpenOrTuck: Boolean = false,
missingBracesIndent: Set[Int] = Set.empty,
replace: String = null
) {
def hasBreakAfter: Boolean = state.split.isNL
Expand Down
Loading

0 comments on commit 5935475

Please sign in to comment.