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

ScalafmtConfig: allow fileOverride shortcuts, set cross-build dialects #2902

Merged
merged 2 commits into from
Nov 23, 2021
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
38 changes: 36 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -3466,6 +3466,25 @@ project {
}
```

#### `project.layout`

Allows specifying a project structure naming convention which can be used to
select an appropriate dialect for cross-building if one is explicitly selected
via [`fileOverride`](#fileoverride). By default, it's disabled (`null`).

Currently, the following options are supported:

- (since v3.2.0) `StandardConvention`: this is the usual naming convention
putting scala source code under `src/main/scala` or `src/test/scala`, with
alternate cross-build dialects in `src/main/scala-2.13`

If this parameter is set, some supported dialects will be determined automatically;
if the detected dialect is compatible with the overall one (`runner.dialect`),
no change will be applied.

Currently, supports scala binary versions 2.10-2.13 as well as 3; also, if the version
is major scala 2 (i.e., `scala-2`), will select the scala 2.13 dialect.

### `fileOverride`

> Since v2.5.0.
Expand All @@ -3477,10 +3496,10 @@ pattern. For instance,
```
align.preset = none
fileOverride {
"glob:**/*.sbt" {
"glob:**.sbt" {
align.preset = most
}
"glob:**/src/test/scala/**/*.scala" {
"glob:**/src/test/scala/**.scala" {
maxColumn = 120
binPack.unsafeCallSite = true
}
Expand All @@ -3493,6 +3512,21 @@ suites.

> This parameter does not modify which files are formatted.

File names will be matched against the patterns in the order in which they are
specified in the configuration file, in case multiple patterns match a given file.

The parameter also allows the following shortcuts:

- (since v3.2.0) setting only the dialect:
- `fileOverride { "glob:**/*.sbt" = sbt1 }`
- (since v3.2.0) setting based on the file extension:
- `fileOverride { ".sbt" { runner.dialect = sbt1 } }`
- this is simply a shortcut for `glob:**.ext`
- (since v3.2.0) setting based on the language:
- `fileOverride { "lang:scala-2" = scala213 }`
- requires [project.layout](#projectlayout) (sets dialect for minor versions)
- these patterns will be matched last

## Spaces

### `spaces.beforeContextBoundColon`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ case class NamedDialect(name: String, dialect: Dialect)
object NamedDialect {

def apply(pair: sourcecode.Text[Dialect]): NamedDialect = {
apply(pair.source.toLowerCase, pair.value)
val name = pair.source.substring(pair.source.lastIndexOf('.') + 1)
apply(name.toLowerCase, pair.value)
}

val scala212 = Scala212
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ package org.scalafmt.config

import java.nio.file

import scala.annotation.tailrec
import scala.meta.Dialect
import scala.meta.dialects

import org.scalafmt.CompatCollections.JavaConverters._
import org.scalafmt.sysops.AbsoluteFile
import org.scalafmt.sysops.OsSpecific._

Expand All @@ -10,6 +15,7 @@ import metaconfig.annotation.DeprecatedName

case class ProjectFiles(
git: Boolean = false,
layout: Option[ProjectFiles.Layout] = None,
includePaths: Seq[String] = ProjectFiles.defaultIncludePaths,
excludePaths: Seq[String] = Nil,
@DeprecatedName(
Expand Down Expand Up @@ -84,4 +90,70 @@ object ProjectFiles {
matchesPath(file.path)
}

sealed abstract class Layout {
def getLang(path: AbsoluteFile): Option[String]
def getDialectByLang(lang: String)(implicit
dialect: Dialect
): Option[NamedDialect]
}

object Layout {

case object StandardConvention extends Layout {
private val phaseLabels = Seq("main", "test", "it")

override def getLang(path: AbsoluteFile): Option[String] = {
val dirsIter = path.path.getParent.iterator().asScala
val dirs = dirsIter.map(_.toString).toArray
getLang(dirs, dirs.length)
}

@tailrec
private def getLang(dirs: Array[String], len: Int): Option[String] = {
val srcIdx = dirs.lastIndexOf("src", len - 3)
if (srcIdx < 0) None
else {
val langIdx = srcIdx + 2
val found = phaseLabels.contains(dirs(srcIdx + 1))
if (found) Some(dirs(langIdx)) else getLang(dirs, langIdx)
}
}

@inline private def is211(implicit dialect: Dialect) =
!dialect.allowCaseClassWithoutParameterList
@inline private def is212(implicit dialect: Dialect) =
dialect.allowTrailingCommas
@inline private def is213(implicit dialect: Dialect) =
dialect.allowLiteralTypes
@inline private def is3(implicit dialect: Dialect) =
dialect.allowSignificantIndentation

@inline private def nd(text: sourcecode.Text[Dialect]) =
Some(NamedDialect(text))
private val s210 = nd(dialects.Scala210)
private val s211 = nd(dialects.Scala211)
private val s212 = nd(NamedDialect.scala212)
private val s213 = nd(NamedDialect.scala213)
private val s3 = nd(NamedDialect.scala3)

override def getDialectByLang(lang: String)(implicit
dialect: Dialect
): Option[NamedDialect] = lang match {
case "scala-2.10" if is211 => s210
case "scala-2.11" if is212 || !is211 => s211
case "scala-2.12" if is213 || !is212 => s212
case "scala-2.13" if is3 || !is213 => s213
case "scala-2" if is3 => s213
case "scala-3" if !is3 => s3
case _ => None
}
}

implicit val reader: ConfCodecEx[Layout] =
ReaderUtil.oneOf[Layout](
StandardConvention
)

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ case class ScalafmtConfig(
): ScalafmtConfig =
copy(runner = runner.copy(dialect = dialect))

private[scalafmt] def withDialect(
dialect: Option[NamedDialect]
): ScalafmtConfig =
dialect.fold(this)(withDialect)

def withDialect(dialect: Dialect, name: String): ScalafmtConfig =
withDialect(NamedDialect(name, dialect))

Expand All @@ -160,18 +165,54 @@ case class ScalafmtConfig(
def forSbt: ScalafmtConfig = copy(runner = runner.forSbt)

private lazy val expandedFileOverride = Try {
val langPrefix = "lang:"
val param = fileOverride.values.filter(_._1.nonEmpty)
val hasLayout = project.layout.isDefined
val patStyles = param.map { case (pat, conf) =>
val isLang = hasLayout && pat.startsWith(langPrefix)
val eitherPat =
if (isLang) Left(pat.substring(langPrefix.length)) else Right(pat)
val cfg = conf match {
case x: Conf.Str => withDialect(NamedDialect.codec.read(None, x).get)
case x =>
val dialectOpt = eitherPat.left.toOption.flatMap { lang =>
project.layout.flatMap(_.getDialectByLang(lang)(dialect))
}
decoder.read(Some(withDialect(dialectOpt)), x).get
}
eitherPat -> cfg
}
val langResult = patStyles.collect { case (Left(lang), cfg) => lang -> cfg }
val fs = file.FileSystems.getDefault
fileOverride.values.map { case (pattern, conf) =>
val style = decoder.read(Some(this), conf).get
fs.getPathMatcher(pattern.asFilename) -> style
val pmResult = patStyles.collect { case (Right(pat), cfg) =>
val pattern = if (pat(0) == '.') "glob:**" + pat else pat.asFilename
fs.getPathMatcher(pattern) -> cfg
}
(langResult, pmResult)
}

def getConfigFor(filename: String): Try[ScalafmtConfig] = {
val absfile = AbsoluteFile(FileOps.getFile(filename))
expandedFileOverride.map { x =>
x
.collectFirst { case (pm, style) if pm.matches(absfile.path) => style }
.getOrElse(this)
@inline def otherDialect(style: ScalafmtConfig): Boolean =
style.runner.getDialect ne runner.getDialect
def onLang[A](f: (ProjectFiles.Layout, String) => A): Option[A] =
project.layout.flatMap { layout =>
layout.getLang(absfile).map { lang => f(layout, lang) }
}
expandedFileOverride.map { case (langStyles, pmStyles) =>
def langStyle = onLang { (layout, lang) =>
val style = langStyles.collectFirst { case (`lang`, style) => style }
style.getOrElse(withDialect(layout.getDialectByLang(lang)(dialect)))
}
val pmStyle = pmStyles.collectFirst {
case (pm, style) if pm.matches(absfile.path) =>
if (otherDialect(style)) style
else
style.withDialect(onLang {
_.getDialectByLang(_)(style.dialect)
}.flatten)
}
pmStyle.orElse(langStyle).getOrElse(this)
}
}

Expand Down
139 changes: 139 additions & 0 deletions scalafmt-tests/src/test/resources/test/Dialect.source
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,142 @@ def anotherMethod(): Unit = doOtherStuff()
def myMethod(): Unit = doSomeStuff()
@main
def anotherMethod(): Unit = doOtherStuff()
<<< #2882 scala-3, explicit
runner.dialect = scala213
project.layout = StandardConvention
fileOverride { "lang:scala-3" = scala3 }
===
trait A /* comm */ :
val cond =
if true then
stat1
stat2
else { // c1
stat3
stat4
}
end if
>>> foo/src/main/scala-3/bar.scala
trait A /* comm */:
val cond =
if true then
stat1
stat2
else { // c1
stat3
stat4
}
end if
<<< #2882 scala-3, implicit
runner.dialect = scala213
project.layout = StandardConvention
===
trait A /* comm */ :
val cond =
if true then
stat1
stat2
else { // c1
stat3
stat4
}
end if
>>> foo/src/main/scala-3/bar.scala
trait A /* comm */:
val cond =
if true then
stat1
stat2
else { // c1
stat3
stat4
}
end if
<<< #2882 scala-2, explicit
runner.dialect = scala211
project.layout = StandardConvention
fileOverride { "lang:scala-2" = scala213 }
===
object a {
foo(
bar,
baz, // trailing comma
)
}
>>> foo/src/main/scala-2/bar.scala
object a {
foo(
bar,
baz // trailing comma
)
}
<<< #2882 scala-2, implicit with 3
runner.dialect = scala3
project.layout = StandardConvention
===
object a {
do { foo } while (bar) // unsupported in scala3
}
>>> foo/src/main/scala-2/bar.scala
object a {
do { foo } while (bar) // unsupported in scala3
}
<<< #2882 scala-2, implicit with 213
runner.dialect = scala213
project.layout = StandardConvention
===
object a {
foo(
bar,
baz, // trailing comma
)
}
>>> foo/src/main/scala-2/bar.scala
object a {
foo(
bar,
baz // trailing comma
)
}
<<< #2882 scala-2.10, implicit
runner.dialect = scala211
project.layout = StandardConvention
===
case class a { // unsupported since 2.11
def foo = 1
}
>>> foo/src/main/scala-2.10/bar.scala
case class a { // unsupported since 2.11
def foo = 1
}
<<< #2882 scala-2.13, implicit
runner.dialect = scala211
project.layout = StandardConvention
fileOverride { "lang:scala-2.13" = { trailingCommas = always } }
===
object a {
foo(
bar,
baz, // trailing comma
)
}
>>> foo/src/main/scala-2.13/bar.scala
object a {
foo(
bar,
baz, // trailing comma
)
}
<<< #2882 .sbt
runner.dialect = scala211
fileOverride { ".sbt" = scala213 }
===
foo(
bar,
baz, // trailing comma
)
>>> foo/bar/baz.sbt
foo(
bar,
baz // trailing comma
)