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

Allow configuring the dialect in Scalafix #1373

Merged
merged 6 commits into from
Apr 29, 2021
Merged

Conversation

mlachkar
Copy link
Collaborator

@mlachkar mlachkar commented Apr 16, 2021

Scala3 dialect is a superset of Scala213 dialect:

implicit val Scala3 = Scala213
    .withAllowAndTypes(true)
    // there 3 different ways to specify vargs, some will be removed in future Scala 3 versions
    .withAllowAtForExtractorVarargs(true) // both @ and : work currently for Scala 3
    .withAllowColonForExtractorVarargs(true) // both @ and : work currently for Scala 3
    .withAllowPostfixStarVarargSplices(true)
    .withAllowEnums(true)
    .withAllowImplicitByNameParameters(true)
    .withAllowInlineMods(true)
    .withAllowLiteralTypes(true)
    .withAllowMethodTypes(false)
    .withAllowOrTypes(true) // Scala 3: `val a: A | B`
    .withAllowTrailingCommas(true)
    .withAllowTraitParameters(true)
    .withAllowTypeLambdas(true)
    .withAllowViewBounds(false) // View bounds have been removed in Dotty
    .withAllowXmlLiterals(false) // Scala 3: parser doesn't support xml
    .withAllowGivenUsing(true)
    .withAllowExtensionMethods(true)
    .withAllowOpenClass(true)
    .withAllowSpliceAndQuote(true)
    .withAllowToplevelStatements(true)
    .withAllowOpaqueTypes(true)
    .withAllowExportClause(true)
    .withAllowCommaSeparatedExtend(true)
    .withAllowEndMarker(true)
    .withAllowInterpolationDolarQuoteEscape(true)
    .withAllowSignificantIndentation(true)
    .withAllowQuestionMarkPlaceholder(true)
    .withAllowTypeParamUnderscore(false)
    .withAllowByNameRepeatedParameters(true)
    .withAllowLazyValAbstractValues(true)
    .withAllowUpperCasePatternVarBinding(true)
    .withAllowDerives(true)
    .withAllowTypeInBlock(true)
    .withAllowPolymorphicFunctions(true)
    .withAllowMatchAsOperator(true)
    .withAllowTypeMatch(true)
    .withAllowInfixMods(true)
    .withAllowSymbolLiterals(false)
    .withAllowDependentFunctionTypes(true)
    .withAllowAllTypedPatterns(true)
    .withAllowAsForImportRename(true)
    .withAllowStarWildcardImport(true)
    ```

    Maybe we don't need a new parameter to choose which dialect. 
    
    

@@ -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
Copy link
Collaborator Author

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 ?

Copy link
Collaborator

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(
Copy link
Collaborator Author

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)

Copy link
Collaborator

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?

Copy link
Contributor

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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,
Copy link
Collaborator Author

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,
Copy link
Collaborator Author

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,
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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

@mlachkar mlachkar requested a review from bjaglin April 16, 2021 11:44
@tgodzik
Copy link
Contributor

tgodzik commented Apr 16, 2021

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:

  .withAllowViewBounds(false) // View bounds have been removed in Dotty

I would rather add another option to .scalafix.conf.

Copy link
Collaborator

@bjaglin bjaglin left a 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
Copy link
Collaborator

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(
Copy link
Collaborator

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](
Copy link
Collaborator

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?

@tgodzik
Copy link
Contributor

tgodzik commented Apr 19, 2021

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?

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.

@tgodzik
Copy link
Contributor

tgodzik commented Apr 19, 2021

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?

@bjaglin
Copy link
Collaborator

bjaglin commented Apr 21, 2021

Any thought on this correctness vs simplicity dilemma @olafurpg?

@olafurpg
Copy link
Contributor

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.

@mlachkar
Copy link
Collaborator Author

mlachkar commented Apr 26, 2021

in scalafix.conf, we can already provide a Dialect. In this commit, I just make sure that it's used to parse files.
This commit dosn't add the possibilit to configure the dialect through the CLI.

@mlachkar mlachkar requested a review from bjaglin April 26, 2021 13:50
@mlachkar mlachkar force-pushed the dialect branch 2 times, most recently from 8dbaacc to c275892 Compare April 26, 2021 15:33
Copy link
Collaborator

@bjaglin bjaglin left a 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
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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,
Copy link
Collaborator

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

project/ScalafixBuild.scala Outdated Show resolved Hide resolved
Comment on lines 16 to 18
decoder[Symbol.Global](field = "symbol")(
ScalafixMetaconfigReaders.symbolGlobalReader
)
Copy link
Collaborator

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes

Copy link
Collaborator Author

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
Copy link
Collaborator

Choose a reason for hiding this comment

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

🎉

@mlachkar mlachkar force-pushed the dialect branch 2 times, most recently from dd3add7 to b505cd6 Compare April 27, 2021 08:19
@mlachkar
Copy link
Collaborator Author

mlachkar commented Apr 27, 2021

Still missing: the CLI.
We can deduce the dialect using the scalaVersion if present, which would override the dialect configured in .scalafix.conf.
or we can add the argument dialect in cli, and take it into account.

I don't know what's the best solution.

Comment on lines 126 to 129
@Description(
"default Scala213" +
"Possibilities: Scala3, Scala213"
)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Need to be improved

Copy link
Collaborator

@bjaglin bjaglin left a 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
Copy link
Collaborator

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)

Copy link
Collaborator Author

@mlachkar mlachkar Apr 29, 2021

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?

Copy link
Collaborator

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?

Copy link
Collaborator

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

Copy link
Collaborator

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

Copy link
Collaborator Author

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")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
@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,
Copy link
Collaborator

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" +
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
"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"
Copy link
Collaborator

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?

Copy link
Collaborator Author

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

@@ -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
Copy link
Collaborator

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

Suggested change
// just to launch first test in scala 3

@mlachkar mlachkar changed the title Make scala3 dialect the default one Allow configuring the dialect in Scalafix Apr 29, 2021
@mlachkar mlachkar merged commit bf0df22 into scalacenter:main Apr 29, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants