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

Implement SortModifiers rewrite rule #1140

Merged
47 changes: 47 additions & 0 deletions readme/Configuration.scalatex
Expand Up @@ -684,6 +684,53 @@
@demoStyle(rewriteAsciiImports)
import foo.{~>, `symbol`, bar, Random}

@sect(Rewrite.rewrite2name(SortModifiers))
@p
Modifiers are sorted based on the given order. Affects modifiers of the following
definitions: trait, class, object, type, and val+var, both as fields and class parameters.

Demo run. On the left you have the code before sorting, on the right after.
@demoStyle(rewriteSortModifiers)
sealed trait ADT
final private object ADTs {

private final type T1 = Int
final private[ADTs] type T2 = String

final private implicit object ADT1 extends ADT
implicit private final object ADT2 extends ADT
final implicit private object ADT3 extends ADT

abstract sealed class ADTA(val name: T1) extends ADT {
def test: T1
}

final class ADT4(private[ADTs] final implicit var impl: T2) extends ADT {
lazy private[ADT4] implicit override val test: T1 = 42
}
}

If you choose the non-default sort order then you have to specify all eight modifiers in the order you wish to see
them. Hint: since some modifiers are mutually exclusive, you might want to order them next to each other.


@p
Example config:
@cliFlags
rewrite {
rules = [SortModifiers]
#optional, see default values below
sortModifiers {
order = ["implicit", "final", "sealed", "abstract",
"override", "private", "protected", "lazy"]
}
}

Default values:
@ul
@li
@code{rewrite.sortModifiers.order} = @rewriteSortModifiersDefaultString

@sect(Rewrite.rewrite2name(PreferCurlyFors))
@p
Replaces parentheses into curly braces in for comprehensions that
Expand Down
24 changes: 24 additions & 0 deletions readme/src/main/scala/org/scalafmt/readme/Readme.scala
Expand Up @@ -159,6 +159,30 @@ object Readme {
rules = Seq(AsciiSortImports)
))

val rewriteSortModifiers =
ScalafmtConfig.default120.copy(
rewrite = ScalafmtConfig.default.rewrite.copy(
rules = Seq(SortModifiers)
))

/**
* This looks way too hacky. But can't seem to find a typeclass
* that ought to "encode" the ``ModKey`` enum.
*
* Additionally, a Vector of Strings is simply concatenated, hence
* the extra .mkString.
* {{{
* [error] found : Vector[org.scalafmt.config.SortSettings.ModKey]
* [error] required: scalatags.Text.Modifier
* [error] (which expands to) scalatags.generic.Modifier[scalatags.text.Builder]
* [error] @code{rewrite.sortModifiers.order} = @rewriteSortModifiers.rewrite.sortModifiers.order
* }}}
*/
val rewriteSortModifiersDefaultString =
SortSettings.defaultOrder
.map(_.productPrefix)
.mkString("[\"", "\", \"", "\"]")

val rewritePreferCurlyFors =
ScalafmtConfig.default.copy(
rewrite = ScalafmtConfig.default.rewrite.copy(
Expand Down
Expand Up @@ -7,16 +7,29 @@ import metaconfig.Configured._

object ReaderUtil {
// Poor mans coproduct reader
def oneOf[T: ClassTag](options: sourcecode.Text[T]*): ConfDecoder[T] = {
val m = options.map(x => x.source.toLowerCase() -> x.value).toMap
def oneOf[T: ClassTag](options: sourcecode.Text[T]*): ConfDecoder[T] =
oneOfImpl(lowerCase, options)

def oneOfIgnoreBackticks[T: ClassTag](
options: sourcecode.Text[T]*): ConfDecoder[T] =
oneOfImpl(lowerCaseNoBackticks, options)

private val lowerCase: String => String = s => s.toLowerCase
Copy link
Member

Choose a reason for hiding this comment

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

Why not def lowerCase(s: String): String = s.toLowercase?

There are many reasons to use methods instead of functions. Function declarations are harder to read, produce more cryptic stack traces, val in objects are evaluated during global initialization potentially causing NullPointerException, val in object creates a JVM field + method while methods creates only one JVM method.

private val lowerCaseNoBackticks: String => String = s =>
s.toLowerCase().replace("`", "")

private def oneOfImpl[T: ClassTag](
sanitize: String => String,
options: Seq[sourcecode.Text[T]]): ConfDecoder[T] = {
val m = options.map(x => sanitize(x.source) -> x.value).toMap
ConfDecoder.instance[T] {
case Conf.Str(x) =>
m.get(x.toLowerCase()) match {
m.get(sanitize(x)) match {
case Some(y) =>
Ok(y)
case None =>
val available = options.map(_.source).mkString(", ")
val msg = s"Unknown input '$x'. Expected one of $available"
val available = m.keys.mkString(", ")
val msg = s"Unknown input '$x'. Expected one of: $available"
ConfError.msg(msg).notOk
}
}
Expand Down
Expand Up @@ -9,7 +9,8 @@ case class RewriteSettings(
rules: Seq[Rewrite] = Nil,
@Recurse redundantBraces: RedundantBracesSettings =
RedundantBracesSettings(),
@Recurse neverInfix: Pattern = Pattern.neverInfix
@Recurse neverInfix: Pattern = Pattern.neverInfix,
@Recurse sortModifiers: SortSettings = SortSettings.default
) {
Rewrite.validateRewrites(rules) match {
case Nil => // OK
Expand All @@ -21,4 +22,12 @@ case class RewriteSettings(
)
}

if (sortModifiers.order.distinct.length != 8)
Copy link
Member

Choose a reason for hiding this comment

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

It's better to write a decoder by hand in the SortSettings companion, the values are encoded from Conf.Lst(values) where values should be only Conf.Str.

The Rewrite.validateRewrites(rules) validation above is a hack that should not be repeated, I'm cleaning up configuration in #1145.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, this is fine, I can take care of it in #1145 once this PR is merged :)

throw InvalidScalafmtConfiguration(
new IllegalArgumentException(
"'sortModifiers.order', if specified, it has to contain all of the following values in the order you wish them sorted:" +
"""["private", "protected" , "abstract", "final", "sealed", "implicit", "override", "lazy"]"""
)
)

}
@@ -0,0 +1,53 @@
package org.scalafmt.config

import metaconfig._

@DeriveConfDecoder
Copy link
Member

Choose a reason for hiding this comment

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

Best to replace this with a hand-written decoder that validates that all modifiers are defined, and produces a helpful error message saying which modifier is missing in case something is wrong.

Copy link
Member

Choose a reason for hiding this comment

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

Nevermind this comment.

case class SortSettings(
order: Vector[SortSettings.ModKey]
)

object SortSettings {

implicit val SortSettingsModKeyReader: ConfDecoder[ModKey] =
ReaderUtil.oneOfIgnoreBackticks[ModKey](
`implicit`,
`final`,
`sealed`,
`abstract`,
`override`,
`private`,
`protected`,
`lazy`
)

val defaultOrder: Vector[ModKey] =
Copy link
Collaborator

Choose a reason for hiding this comment

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

is there any reason why the default order doesn't follow standard scala style:

https://docs.scala-lang.org/style/declarations.html#modifiers

Specifically, final should precede lazy.

Copy link
Member

Choose a reason for hiding this comment

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

I have no strong preference, defaulting to the scala style guide sounds good 👍

Vector(
`implicit`,
//
`final`,
`sealed`,
`abstract`,
//
`override`,
//
`private`,
`protected`,
//
`lazy`
)

def default: SortSettings =
SortSettings(defaultOrder)

sealed trait ModKey extends Product

case object `private` extends ModKey
case object `protected` extends ModKey
case object `final` extends ModKey
case object `sealed` extends ModKey
case object `abstract` extends ModKey
case object `implicit` extends ModKey
case object `override` extends ModKey
case object `lazy` extends ModKey
}
Expand Up @@ -31,7 +31,8 @@ object Rewrite {
AsciiSortImports,
PreferCurlyFors,
ExpandImportSelectors,
AvoidInfix
AvoidInfix,
SortModifiers
)

private def nameMap[T](t: sourcecode.Text[T]*): Map[String, T] = {
Expand All @@ -45,7 +46,8 @@ object Rewrite {
AsciiSortImports,
PreferCurlyFors,
ExpandImportSelectors,
AvoidInfix
AvoidInfix,
SortModifiers
)
val rewrite2name: Map[Rewrite, String] = name2rewrite.map(_.swap)
val available = Rewrite.name2rewrite.keys.mkString(", ")
Expand Down
@@ -0,0 +1,84 @@
package org.scalafmt.rewrite

import org.scalafmt.config.SortSettings._

import scala.meta.Tree
import scala.meta._

object SortModifiers extends Rewrite {

override def rewrite(code: Tree, ctx: RewriteCtx): Seq[Patch] = {
implicit val order = ctx.style.rewrite.sortModifiers.order

/*
* in the case of Class, Object, and of class constructor parameters
* some Mods are immovable, e.g. 'case' in "case class X".
*
* The case of parameters is a bit more curious because there the
* "val" or "var" in, say:
* {{{
* class Test(private final val x: Int)
* }}}
* are considered Mods, instead of being similar to `Defn.Val`, or `Defn.Var`.
*/
val patchesOfPatches = code.collect {
case d: Decl.Def => sortMods(d.mods)
case v: Decl.Val => sortMods(v.mods)
case v: Decl.Var => sortMods(v.mods)
case t: Decl.Type => sortMods(t.mods)
case d: Defn.Def => sortMods(d.mods)
case v: Defn.Val => sortMods(v.mods)
case v: Defn.Var => sortMods(v.mods)
case t: Defn.Type => sortMods(t.mods)
case c: Defn.Class => sortMods(c.mods.filterNot(_.is[Mod.Case]))
case o: Defn.Object => sortMods(o.mods.filterNot(_.is[Mod.Case]))
case t: Defn.Trait => sortMods(t.mods)
case p: Term.Param =>
sortMods(
p.mods.filterNot(m => m.is[Mod.ValParam] || m.is[Mod.VarParam]))
}
patchesOfPatches.flatten
}

private def sortMods(
oldMods: Seq[Mod]
)(implicit order: Vector[ModKey]): Seq[Patch] = {
if (oldMods.isEmpty) Nil
else {
val sortedMods: Seq[Mod] = oldMods.sortWith(orderModsBy(order))
sortedMods.zip(oldMods).flatMap {
case (next, old) =>
if (old.tokens.isEmpty) {
//required for cases like: def foo(implicit x: Int)
Nil
} else {
val removeOld = old.tokens.map(t => TokenPatch.Remove(t))
val addNext = TokenPatch.AddRight(old.tokens.head, next.syntax)
removeOld :+ addNext
}
}
}
}

/**
* @return
* m1 < m2; according to the order given by the Vector
*/
private def orderModsBy(order: Vector[ModKey])(m1: Mod, m2: Mod): Boolean = {
val idx1 = order.indexWhere(modCorrespondsToSettingKey(m1))
val idx2 = order.indexWhere(modCorrespondsToSettingKey(m2))
idx1 < idx2
}

private def modCorrespondsToSettingKey(m: Mod)(p: ModKey): Boolean = {
p == `private` && m.is[Mod.Private] ||
p == `protected` && m.is[Mod.Protected] ||
p == `final` && m.is[Mod.Final] ||
p == `sealed` && m.is[Mod.Sealed] ||
p == `abstract` && m.is[Mod.Abstract] ||
p == `lazy` && m.is[Mod.Lazy] ||
p == `implicit` && m.is[Mod.Implicit] ||
p == `override` && m.is[Mod.Override]
}

}