Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Router: merge classic select chain rule with other #2067

Merged
merged 3 commits into from
Jul 5, 2020
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -138,25 +138,6 @@ class FormatOps(val tree: Tree, baseStyle: ScalafmtConfig) {
ownersMap.get(hash(tok)).map(tree => tok -> tree)
}

object `:chain:` {
def unapply(
tok: Token
)(implicit style: ScalafmtConfig): Option[(Token, Vector[Term.Select])] = {
val ft = tokens(tok)
ft.meta.leftOwner match {
case t: Term.Select =>
val (expireTree, nextSelect) =
findLastApplyAndNextSelect(t, style.optIn.encloseClassicChains)
if (!canStartSelectChain(t, nextSelect, expireTree)) None
else {
val chain = getSelectChain(t, expireTree, Vector(t))
Some(tok -> chain)
}
case _ => None
}
}
}

@inline def prev(tok: FormatToken): FormatToken = tokens(tok, -1)
@inline def next(tok: FormatToken): FormatToken = tokens(tok, 1)

Expand Down Expand Up @@ -465,48 +446,12 @@ class FormatOps(val tree: Tree, baseStyle: ScalafmtConfig) {
}
}

/**
* Returns last token of select, handles case when select's parent is apply.
*
* For example, in:
* foo.bar[T](1, 2)
* the last token is the final )
*
* @param dot the dot owned by the select.
*/
def getSelectsLastToken(dot: T.Dot): FormatToken = {
var curr = tokens(dot, 1)
while (
isOpenApply(
curr.right,
includeCurly = true,
includeNoParens = true
) &&
!statementStarts.contains(hash(curr.right))
) {
if (curr.right.is[T.Dot]) {
curr = tokens(curr, 2)
} else {
curr = tokens(matching(curr.right))
}
}
curr
}

def getOptimalTokenFor(token: Token): Token =
getOptimalTokenFor(tokens(token))

def getOptimalTokenFor(ft: FormatToken): Token =
if (isAttachedSingleLineComment(ft)) ft.right else ft.left

def getSelectOptimalToken(tree: Tree): Token = {
val lastDotOpt = findLast(tree.tokens)(_.is[T.Dot])
if (lastDotOpt.isEmpty)
throw new IllegalStateException(s"Missing . in select $tree")
val lastDot = lastDotOpt.get.asInstanceOf[T.Dot]
lastToken(getSelectsLastToken(lastDot).meta.leftOwner)
}

def infixIndent(
app: InfixApp,
formatToken: FormatToken,
Expand Down Expand Up @@ -1009,22 +954,21 @@ class FormatOps(val tree: Tree, baseStyle: ScalafmtConfig) {
val owners = chain.fold[Set[Tree]](Set(_), x => x.toSet)
val nlPolicy = ctorWithChain(owners, lastToken)
val nlOnelineTag = style.binPack.parentConstructors match {
case BinPack.ParentCtors.Oneline => SplitTag.Active
case BinPack.ParentCtors.Oneline => Right(true)
case BinPack.ParentCtors.OnelineIfPrimaryOneline =>
SplitTag.OnelineWithChain
Left(SplitTag.OnelineWithChain)
case BinPack.ParentCtors.Always | BinPack.ParentCtors.Never =>
SplitTag.Ignored
Right(false)
case BinPack.ParentCtors.MaybeNever =>
val isOneline = style.newlines.sourceIs(Newlines.fold)
if (isOneline) SplitTag.Active else SplitTag.Ignored
Right(style.newlines.sourceIs(Newlines.fold))
}
val indent = Indent(Num(indentLen), lastToken, ExpiresOn.After)
val extendsThenWith = chain.left.exists(_.inits.length > 1)
Seq(
Split(Space, 0).withSingleLine(lastToken, noSyntaxNL = extendsThenWith),
Split(nlMod, 0)
.onlyFor(nlOnelineTag)
.activateFor(nlOnelineTag)
.onlyIf(nlOnelineTag != Right(false))
.preActivateFor(nlOnelineTag.left.toOption)
.withSingleLine(lastToken, noSyntaxNL = extendsThenWith)
.withIndent(indent),
Split(nlMod, 1).withPolicy(nlPolicy).withIndent(indent)
Expand Down Expand Up @@ -1480,6 +1424,22 @@ class FormatOps(val tree: Tree, baseStyle: ScalafmtConfig) {
})
}

/** Checks if an earlier select started the chain */
@tailrec
final def inSelectChain(
prevSelect: Option[Term.Select],
thisSelect: Term.Select,
lastApply: Tree
)(implicit style: ScalafmtConfig): Boolean =
prevSelect match {
case None => false
case Some(p) if canStartSelectChain(p, Some(thisSelect), lastApply) =>
true
case Some(p) =>
val prevPrevSelect = findPrevSelect(p, style.encloseSelectChains)
inSelectChain(prevPrevSelect, p, lastApply)
}

@tailrec
final def findTokenWith[A](
ft: FormatToken,
Expand Down
196 changes: 110 additions & 86 deletions scalafmt-core/shared/src/main/scala/org/scalafmt/internal/Router.scala
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ class Router(formatOps: FormatOps) {
val forceNewlineBeforeExtends = Policy(expire) {
case Decision(t @ FormatToken(_, _: T.KwExtends, _), s)
if t.meta.rightOwner == leftOwner =>
s.filter(x => x.isNL && (x.activeTag ne SplitTag.OnelineWithChain))
s.filter(x => x.isNL && !x.isActiveFor(SplitTag.OnelineWithChain))
}
val policyEnd = defnBeforeTemplate(leftOwner).fold(r)(_.tokens.last)
val policy = delayedBreakPolicy(policyEnd)(forceNewlineBeforeExtends)
Expand Down Expand Up @@ -1194,15 +1194,117 @@ class Router(formatOps: FormatOps) {
}.isDefined =>
Seq(Split(NoSplit, 0))

case t @ FormatToken(left, _: T.Dot, _)
if !style.newlines.sourceIs(Newlines.classic) &&
rightOwner.is[Term.Select] =>
case t @ FormatToken(left, _: T.Dot, _) if rightOwner.is[Term.Select] =>
val enclosed = style.encloseSelectChains
val (expireTree, nextSelect) =
findLastApplyAndNextSelect(rightOwner, true)
val prevSelect = findPrevSelect(rightOwner.asInstanceOf[Term.Select])
findLastApplyAndNextSelect(rightOwner, enclosed)
val thisSelect = rightOwner.asInstanceOf[Term.Select]
val prevSelect = findPrevSelect(thisSelect, enclosed)
val expire = lastToken(expireTree)

def breakOnNextDot: Policy =
nextSelect.fold[Policy](Policy.NoPolicy) { tree =>
val end = tree.name.tokens.head
Policy(end) {
case Decision(t @ FormatToken(_, _: T.Dot, _), s)
if t.meta.rightOwner eq tree =>
val filtered = s.flatMap { x =>
val y = x.activateFor(SplitTag.SelectChainSecondNL)
if (y.isActive) Some(y) else None
}
if (filtered.isEmpty) Seq.empty
else {
val minCost = math.max(0, filtered.map(_.cost).min - 1)
filtered.map { x =>
val p =
x.policy.filter(!_.isInstanceOf[PenalizeAllNewlines])
x.copy(cost = x.cost - minCost, policy = p)
}
}
}
}
val baseSplits = style.newlines.source match {
case Newlines.classic =>
def getNlMod = {
val endSelect = nextSelect.fold(expire)(x => lastToken(x.qual))
val nlAlt = ModExt(NoSplit).withIndent(-2, endSelect, After)
NewlineT(alt = Some(nlAlt))
}

val prevChain = inSelectChain(prevSelect, thisSelect, expireTree)
if (canStartSelectChain(thisSelect, nextSelect, expireTree)) {
val chainExpire =
if (nextSelect.isEmpty) lastToken(thisSelect)
else if (!isEnclosedInMatching(expireTree)) expire
else lastToken(expireTree.tokens.dropRight(1))
val nestedPenalty =
nestedSelect(rightOwner) + nestedApplies(leftOwner)
// This policy will apply to both the space and newline splits, otherwise
// the newline is too cheap even it doesn't actually prevent other newlines.
val penalizeBreaks = penalizeAllNewlines(chainExpire, 2)
def slbPolicy =
SingleLineBlock(
chainExpire,
getExcludeIf(chainExpire),
penaliseNewlinesInsideTokens = true
)
val newlinePolicy = breakOnNextDot & penalizeBreaks
val ignoreNoSplit = t.hasBreak &&
(left.is[T.Comment] || style.optIn.breakChainOnFirstMethodDot)
val chainLengthPenalty =
if (
style.newlines.penalizeSingleSelectMultiArgList &&
nextSelect.isEmpty
) {
// penalize by the number of arguments in the rhs open apply.
// I know, it's a bit arbitrary, but my manual experiments seem
// to show that it produces OK output. The key insight is that
// many arguments on the same line can be hard to read. By not
// putting a newline before the dot, we force the argument list
// to break into multiple lines.
splitCallIntoParts.lift(tokens(t, 2).meta.rightOwner) match {
case Some((_, Left(args))) =>
Math.max(0, args.length - 1)
case Some((_, Right(argss))) =>
Math.max(0, argss.map(_.length).sum - 1)
case _ => 0
}
} else 0
// when the flag is on, penalize break, to avoid idempotence issues;
// otherwise, after the break is chosen, the flag prohibits nosplit
val nlBaseCost =
if (style.optIn.breakChainOnFirstMethodDot && t.noBreak) 3
else 2
val nlCost = nlBaseCost + nestedPenalty + chainLengthPenalty
val nlMod = getNlMod
Seq(
Split(!prevChain, 1) { // must come first, for backwards compat
if (style.optIn.breaksInsideChains) NoSplit.orNL(t.noBreak)
else nlMod
}
.withPolicy(newlinePolicy)
.onlyFor(SplitTag.SelectChainSecondNL),
Split(ignoreNoSplit, 0)(NoSplit)
.withPolicy(slbPolicy, prevChain)
.andPolicy(penalizeBreaks),
Split(if (ignoreNoSplit) Newline else nlMod, nlCost)
.withPolicy(newlinePolicy)
)
} else {
val isComment = left.is[T.Comment]
val doBreak = isComment && t.hasBreak
Seq(
Split(!prevChain, 1) {
if (style.optIn.breaksInsideChains) NoSplit.orNL(t.noBreak)
else if (doBreak) Newline
else getNlMod
}
.withPolicy(breakOnNextDot)
.onlyFor(SplitTag.SelectChainSecondNL),
Split(if (doBreak) Newline else Space(isComment), 0)
)
}

case _ if left.is[T.Comment] =>
Seq(Split(Space.orNL(t.noBreak), 0))

Expand Down Expand Up @@ -1236,7 +1338,7 @@ class Router(formatOps: FormatOps) {
)
}

val delayedBreakPolicy = nextSelect.map { tree =>
val delayedBreakPolicyOpt = nextSelect.map { tree =>
Policy(tree.name.tokens.head) {
case Decision(t @ FormatToken(_, _: T.Dot, _), s)
if t.meta.rightOwner eq tree =>
Expand All @@ -1249,90 +1351,12 @@ class Router(formatOps: FormatOps) {
val willBreak = nextNonCommentSameLine(tokens(t, 2)).right.is[T.Comment]
val splits = baseSplits.map { s =>
if (willBreak || s.isNL) s.withIndent(indent)
else s.andPolicyOpt(delayedBreakPolicy)
else s.andFirstPolicyOpt(delayedBreakPolicyOpt)
}

if (prevSelect.isEmpty) splits
else baseSplits ++ splits.map(_.onlyFor(SplitTag.SelectChainFirstNL))

case FormatToken(T.Ident(name), _: T.Dot, _) if isSymbolicName(name) =>
Seq(Split(NoSplit, 0))

case FormatToken(_: T.Underscore, _: T.Dot, _) =>
Seq(Split(NoSplit, 0))

case tok @ FormatToken(left, dot @ T.Dot() `:chain:` chain, _) =>
val nestedPenalty = nestedSelect(rightOwner) + nestedApplies(leftOwner)
val optimalToken = getSelectOptimalToken(chain.last)
val expire =
if (chain.length == 1) lastToken(chain.last)
else optimalToken

def getNewline(ft: FormatToken): NewlineT = {
val (_, nextSelect) = findLastApplyAndNextSelect(
ft.meta.rightOwner,
style.optIn.encloseClassicChains
)
val endSelect = nextSelect.fold(optimalToken)(x => lastToken(x.qual))
val nlAlt = ModExt(NoSplit).withIndent(-2, endSelect, After)
NewlineT(alt = Some(nlAlt))
}

val breakOnEveryDot = Policy(expire) {
case Decision(t @ FormatToken(_, _: T.Dot, _), _)
if chain.contains(t.meta.rightOwner) =>
val mod =
if (style.optIn.breaksInsideChains)
NoSplit.orNL(t.noBreak)
else getNewline(t)
Seq(Split(mod, 1))
}
val exclude = getExcludeIf(expire)
// This policy will apply to both the space and newline splits, otherwise
// the newline is too cheap even it doesn't actually prevent other newlines.
val penalizeNewlinesInApply = penalizeAllNewlines(expire, 2)
val noSplitPolicy =
SingleLineBlock(
expire,
exclude,
penaliseNewlinesInsideTokens = true
) & penalizeNewlinesInApply
val newlinePolicy = breakOnEveryDot & penalizeNewlinesInApply
val ignoreNoSplit =
style.optIn.breakChainOnFirstMethodDot && tok.hasBreak
val chainLengthPenalty =
if (
style.newlines.penalizeSingleSelectMultiArgList &&
chain.length < 2
) {
// penalize by the number of arguments in the rhs open apply.
// I know, it's a bit arbitrary, but my manual experiments seem
// to show that it produces OK output. The key insight is that
// many arguments on the same line can be hard to read. By not
// putting a newline before the dot, we force the argument list
// to break into multiple lines.
splitCallIntoParts.lift(tokens(tok, 2).meta.rightOwner) match {
case Some((_, Left(args))) =>
Math.max(0, args.length - 1)
case Some((_, Right(argss))) =>
Math.max(0, argss.map(_.length).sum - 1)
case _ => 0
}
} else 0
// when the flag is on, penalize break, to avoid idempotence issues;
// otherwise, after the break is chosen, the flag prohibits nosplit
val nlBaseCost =
if (style.optIn.breakChainOnFirstMethodDot && tok.noBreak) 3 else 2
val nlCost = nlBaseCost + nestedPenalty + chainLengthPenalty
Seq(
Split(NoSplit, 0)
.notIf(ignoreNoSplit)
.withPolicy(noSplitPolicy),
Split(if (ignoreNoSplit) Newline else getNewline(tok), nlCost)
.withPolicy(newlinePolicy)
.withIndent(2, optimalToken, After)
)

// ApplyUnary
case tok @ FormatToken(T.Ident(_), Literal(), _)
if leftOwner == rightOwner =>
Expand Down
Loading