-
Notifications
You must be signed in to change notification settings - Fork 186
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
Allow configuring the dialect in Scalafix #1373
Conversation
@@ -8,8 +8,8 @@ import scala.meta.internal.trees.Origin | |||
|
|||
object ScalafixScalametaHacks { | |||
def dialect(language: String): Dialect = | |||
if (language == "Scala") dialects.Scala212 | |||
else if (language.isEmpty) dialects.Scala212 | |||
if (language == "Scala") dialects.Scala3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this method is only used to add an extension method never used in our code base! Maybe we can remove this code and the extension method ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds right, it's under internal
anyway
@@ -13,16 +13,10 @@ case class ParserConfig( | |||
inlineKeyword: Boolean = false | |||
) { | |||
|
|||
private val dialect = scala.meta.dialects.Scala213.copy( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the point of writing our own definition of dialect. this one is not correct.
implicit val Scala213 = Scala212
.withAllowImplicitByNameParameters(true)
.withAllowLiteralTypes(true)
.withAllowNumericLiteralUnderscoreSeparators(true)
.withAllowTryWithAnyExpr(true)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure either - does it make sense to keep ParserConfig
at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC, the motivation for the custom dialect was to allow as much as possible of Scala 3 syntax without introducing regressions for Scala 2 users. Some Scala 3 syntax can be enabled safely while other syntax like significant indent may change the parsed structure of Scala 2 programs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, thanks! I understand better! Clever idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, thanks! I understand better! Clever idea.
@@ -13,16 +13,10 @@ case class ParserConfig( | |||
inlineKeyword: Boolean = false | |||
) { | |||
|
|||
private val dialect = scala.meta.dialects.Scala213.copy( | |||
allowLiteralTypes = literalTypes, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
already true in 2.12
@@ -13,16 +13,10 @@ case class ParserConfig( | |||
inlineKeyword: Boolean = false | |||
) { | |||
|
|||
private val dialect = scala.meta.dialects.Scala213.copy( | |||
allowLiteralTypes = literalTypes, | |||
allowTrailingCommas = trailingCommas, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
already true in 2.12
private val dialect = scala.meta.dialects.Scala213.copy( | ||
allowLiteralTypes = literalTypes, | ||
allowTrailingCommas = trailingCommas, | ||
allowInlineMods = inlineKeyword, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we set allowInlineMods to false, and it's true in scala3
allowLiteralTypes = literalTypes, | ||
allowTrailingCommas = trailingCommas, | ||
allowInlineMods = inlineKeyword, | ||
allowInlineIdents = !inlineKeyword |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
allowInlineIdent is true since 2.10
Hmm... this is true in most cases and I was also thinking about maybe doing this, but it might not be best to use it by default. Especially later on after some syntax will be deprecated then we will for sure need a new dialect. And some things are already unsupported for example:
I would rather add another option to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scala3 dialect is a superset of Scala213 dialect:
I guess this is a wrong assumption, so scalafix needs to know whether it's dealing with 2.x or 3 sources.
I would rather add another option to .scalafix.conf.
I initially thought we should follow the scalafmt way (optional runner.dialect
key), but scalafix is a bit different, because for semantic rules, the scala version is already mandatory, as a CLI/invocation argument.
So instead of adding a configuration key, we could make that argument mandatory also for syntactic rules. For build tool clients, it's an acceptable breaking change, but for CLI users like https://eed3si9n.com/syntactic-scalafix-rule-for-unified-slash-syntax (maybe a bad example as in this case we look at the extension to infer the dialect), it's quite annoying.
What about keeping the scala version optional for syntactic rules, but when missing try to pars with scala2.x dialect and fall back to scala3 on error (once per invocation no matter how many files we have)? Is there a case where both parsers would succeed but produce different trees?
@@ -8,8 +8,8 @@ import scala.meta.internal.trees.Origin | |||
|
|||
object ScalafixScalametaHacks { | |||
def dialect(language: String): Dialect = | |||
if (language == "Scala") dialects.Scala212 | |||
else if (language.isEmpty) dialects.Scala212 | |||
if (language == "Scala") dialects.Scala3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds right, it's under internal
anyway
@@ -13,16 +13,10 @@ case class ParserConfig( | |||
inlineKeyword: Boolean = false | |||
) { | |||
|
|||
private val dialect = scala.meta.dialects.Scala213.copy( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure either - does it make sense to keep ParserConfig
at all?
@@ -38,14 +38,12 @@ trait ScalafixMetaconfigReaders { | |||
import ScalafixConfig.{DefaultDialect => Default} | |||
import scala.meta.dialects._ | |||
ReaderUtil.oneOf[Dialect]( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where is this ConfDecoder
used?
There are a couple, especially related to optional braces, but mostly if a code was weirdly formatted to start with. object A{
def b =
12
23
} I think in case of Scala 3, 12 and 23 will belong to b, while for Scala 2 only 12 will. |
If possible I would try to declare the Scala version explicitly, but if it turns out to be more problematic for different clients, we could try with the fallback, but maybe issue a warning? |
Any thought on this correctness vs simplicity dilemma @olafurpg? |
I would lean towards requiring users to explicitly declare that the Scala 3 dialect should be used. As Tomasz pointed out, there are subtle risks from enabling full Scala 3 syntax by default. I would avoid heuristics and attempting an automatic fallback. One thing we could do is to customize syntax error messages to recommend enabling the Scala 3 dialect to fix the problem. |
in scalafix.conf, we can already provide a |
8dbaacc
to
c275892
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting there 👍
in scalafix.conf, we can already provide a Dialect. In this commit, I just make sure that it's used to parse files.
Ah great if that's already available. Could you add a unit test to demonstrate this?
This commit dosn't add the possibilit to configure the dialect through the CLI.
For semantic rules, the scala version is mandatory in the CLI args - it should have priority since it means class files exist with that version, so if it does not match with the dialect in the configuration, should we proceed with that one but issue a warning?
allowXmlLiterals = true | ||
) | ||
|
||
val DefaultDialect = scala.meta.dialects.Scala213 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following up on the previous discussion above: this is a breaking change since currently some scala3 syntax can be parsed without tweaking the default. That's what we want but we should document that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can keep the same if needed. I just need to check which one was actually used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so this default is based on 212 and it was only used in old rules (none of the ones we have in scalafix).
the real dialect that was used is this one, where all the additional configuration is already part of scala213.
private val dialect = scala.meta.dialects.Scala213.copy(
allowLiteralTypes = true,
allowTrailingCommas = true,
allowInlineMods = flase,
allowInlineIdents = true
)
I don't know if it's useful to allow scala 3 syntax in scala213, but we could allow some of it.
build.sbt
Outdated
@@ -63,6 +62,7 @@ lazy val core = project | |||
.settings( | |||
moduleName := "scalafix-core", | |||
buildInfoSettingsForCore, | |||
scalacOptions ++= syntheticsOn.value, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of adding it to every scala2 project, can't you keep it in ThisBuild
and remove it for the test-input? that would reduce the noise
decoder[Symbol.Global](field = "symbol")( | ||
ScalafixMetaconfigReaders.symbolGlobalReader | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is that a no-op?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the goal was just to know which implicit is used from ScalafixMetaconfigReaders
object HelloWorld: | ||
|
||
@main def hello = println("Hello, world!") | ||
// This comment is added by Scalafix |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
dd3add7
to
b505cd6
Compare
Still missing: the CLI. I don't know what's the best solution. |
@Description( | ||
"default Scala213" + | ||
"Possibilities: Scala3, Scala213" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to be improved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few minor comments, great work!
Paradise211, | ||
Paradise212 | ||
Scala213, | ||
Scala3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what happened to Sbt1
? I guess it could be useful if someone has a sbt-type file with a different extension than .sbt
(very far-fetched)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
even the old code don't really take sbt
dialect into account.
sbt dialect is always 213 (which is the default) with allowTopLevelItems.
and now sbtDialct is
val DefaultSbtDialect: Dialect = scala.meta.dialects.Sbt1
[Now]
private val sbtDialect = dialect.copy(allowToplevelTerms = true)
[Previously]
// This the code which choose which dialect following the extension of the file
def dialectForFile(path: String): Dialect =
if (path.endsWith(".sbt")) sbtDialect
else dialect
So users are supposed to choose either scala213 or scala3 in dialect. Do you think we should restrict to those two values?
implicit lazy val dialectReader: ConfDecoder[Dialect] = {
import ScalafixConfig.{DefaultDialect => Default}
import scala.meta.dialects._
ReaderUtil.oneOf[Dialect](
Scala213,
Scala3,
//Scala211
//Scala212
)
}
By the way, I thought sbt1 is just sbt with a higher version 1.x.x
Isn't that the case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think we should restrict to those two values?
If we do, then it would be Scala2
and Scala3
, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way, I thought sbt1 is just sbt with a higher version 1.x.x
Isn't that the case?
It is, the format and binary backward compatibility is frozen in 1.x
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
even the old code don't really take sbt dialect into account.
sbt dialect is always 213 (which is the default) with allowTopLevelItems.
Interesting! I think we should rather use the dialect defined in scalameta than defning our own variant here, no?
https://github.com/scalameta/scalameta/blob/6c0a370cc278d473588f973c436d8378a691d2b1/scalameta/dialects/shared/src/main/scala/scala/meta/dialects/package.scala#L70-L72
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes! It's what I did. I Like the idea of having only scala2 and Scala3 dialect !!
@@ -27,19 +29,15 @@ final class SyntacticDocument private[scalafix] ( | |||
} | |||
|
|||
object SyntacticDocument { | |||
@deprecated("use fromInput(input: Input, dialect: Dialect) instead", "0.9.27") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@deprecated("use fromInput(input: Input, dialect: Dialect) instead", "0.9.27") | |
@deprecated("use fromInput(input: Input, dialect: Dialect) instead", "0.9.28") |
build.sbt
Outdated
|
||
inThisBuild( | ||
List( | ||
onLoadMessage := s"Welcome to scalafix ${version.value}", | ||
scalaVersion := scala213, | ||
crossScalaVersions := List(scala213, scala212, scala211), | ||
fork := true, | ||
scalacOptions ++= syntheticsOn.value, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess you can now revert this change and get rid of syntheticsOn
since the scala3 customization is only applied where scala3 cross-building is enabled.
@@ -121,6 +123,11 @@ case class Args( | |||
"The Scala compiler version that was used to compile this project." | |||
) | |||
scalaVersion: String = scala.util.Properties.versionNumberString, | |||
@Description( | |||
"default Scala213" + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"default Scala213" + | |
"Defaults to Scala213 for `.scala` files and Sbt1 for `.sbt` files" + |
@@ -121,6 +123,11 @@ case class Args( | |||
"The Scala compiler version that was used to compile this project." | |||
) | |||
scalaVersion: String = scala.util.Properties.versionNumberString, | |||
@Description( | |||
"default Scala213" + | |||
"Possibilities: Scala3, Scala213" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doesn't metaconfig generate this out of the box?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I don't know
project/ScalafixBuild.scala
Outdated
@@ -108,6 +115,12 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys { | |||
"unit/test:runMain scalafix.tests.util.SaveExpect" :: | |||
s | |||
}, | |||
// just to launch first test in scala 3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is unlikely to be updated when we have more than one test
// just to launch first test in scala 3 |
Scala3 dialect is a superset of Scala213 dialect: