Repository: scalameta/scalameta (scala)

Issue:
Support for selectively overriding individual fields in Dialect
Seems possibly related to #2750
Definitely related to #2734

Now that `Foo[?]` syntax is default, I expected to be able to

```scala
implicit val d = scala.meta.dialects.Scala212.withAllowQuestionMarkPlaceholder(false)
```

... in order to restore the previous functionality, but I'm greeted with 

```
<console>:14: error: dialect of type scala.meta.Dialect is not supported by quasiquotes (to fix this, import something from scala.meta.dialects, e.g. scala.meta.dialects.Scala212)
```

Digging into the code, I found [this message](https://github.com/scalameta/scalameta/blob/3c9b813c542a7fcc165f9be3e238c3085420fab0/scalameta/quasiquotes/shared/src/main/scala/scala/meta/internal/quasiquotes/ReificationMacros.scala#L131-L134):

>     // For now I'll just prohibit quasiquotes for situations when `dialectTree` doesn't point to one of the predefined dialects.
>    // A natural extension to this would be to allow any static value, not just predefined dialects.
>    // Furthermore, we could further relax this restriction by doing parsing for a superset of all dialects and then
>    // delaying validation of resulting ASTs until runtime.

which doesn't inspire much hope. How can this be resolved?

I guess this is only a transitional issue, while everybody upgrades to the latest versions of Scala212 and 213, and I can stay on the old version of scalameta, but I've not seen any discussion around this so I thought I'd ask.

Thank you for your excellent library!

---

A full reproduction case:

```scala
[info] Starting scala interpreter...
Welcome to Scala 2.12.15 (OpenJDK 64-Bit Server VM, Java 11.0.11).
Type in expressions for evaluation. Or try :help.

scala> import scala.meta._
import scala.meta._

scala> q"Foo[_]".syntax
res0: String = Foo[?]  // My consumers can't compile this yet

// An attempt to supply an instance of Dialect is greeted with this message
scala> def foo(implicit dialect: Dialect): String = q"Foo[_]".syntax
<console>:14: error: dialect of type scala.meta.Dialect is not supported by quasiquotes (to fix this, import something from scala.meta.dialects, e.g. scala.meta.dialects.Scala212)
       def foo(implicit dialect: Dialect): String = q"Foo[_]".syntax
                                                    ^
```

Pull request:
Previously, we would always print `?` for the placeholder even if the Scala version wouldn't allow it. Now, we only print `?` if it was explicitely used in the code.

Fixes https://github.com/scalameta/scalameta/issues/2751

The other solution would be introducing another dialect, but that would require a lot of people having to also change that, so it would require a lot more effort in the long run.

Patch:
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala
index 3f16814ca4..ecc2530b6b 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/internal/prettyprinters/TreeSyntax.scala
@@ -15,11 +15,13 @@ import Show.{
 }
 import scala.meta.internal.trees.{root => _, branch => _, _}
 import scala.meta.internal.tokenizers.Chars._
+import scala.meta.internal.trees.Origin.Parsed
 import org.scalameta.adt._
 import org.scalameta.invariants._
 import org.scalameta.unreachable
 import scala.compat.Platform.EOL
 import scala.meta.inputs.Position
+import scala.meta.tokens.Token
 
 object TreeSyntax {
   private final object SyntaxInstances {
@@ -688,7 +690,20 @@ object TreeSyntax {
           s(p(AnyInfixTyp, t.tpe), " ", kw("match"), " {", r(t.cases.map(i(_)), ""), n("}"))
         )
       case t: Type.Placeholder =>
-        if (dialect.allowQuestionMarkPlaceholder) m(SimpleTyp, s(kw("?"), t.bounds))
+        /* In order not to break existing tools `.syntax` should still return
+         * `_` instead `?` unless specifically used.
+         */
+        def isScala3Dialect = dialect.allowSignificantIndentation
+        def questionMarkUsed = t.origin match {
+          case _: Origin.Parsed =>
+            !t.tokens.exists {
+              case _: Token.Underscore => true
+              case _ => false
+            }
+          case _ => false
+        }
+        if (dialect.allowQuestionMarkPlaceholder && (isScala3Dialect || questionMarkUsed))
+          m(SimpleTyp, s(kw("?"), t.bounds))
         else m(SimpleTyp, s(kw("_"), t.bounds))
       case t: Type.Bounds =>
         s(


Test patch:
diff --git a/tests/jvm/src/test/scala/scala/meta/tests/prettyprinters/RegressionSyntaxSuite.scala b/tests/jvm/src/test/scala/scala/meta/tests/prettyprinters/RegressionSyntaxSuite.scala
new file mode 100644
index 0000000000..5776ece335
--- /dev/null
+++ b/tests/jvm/src/test/scala/scala/meta/tests/prettyprinters/RegressionSyntaxSuite.scala
@@ -0,0 +1,58 @@
+package scala.meta.tests.prettyprinters
+
+import scala.meta.tests.parsers.ParseSuite
+
+import scala.meta._
+import munit.TestOptions
+
+class RegressionSyntaxSuite extends ParseSuite {
+
+  def check(
+      testOpts: TestOptions,
+      code: String,
+      expected: String,
+      newDialect: Option[Dialect] = None
+  )(implicit dialect: Dialect) = test(testOpts) {
+    val obtained = code.parse[Stat].get
+    val reprintedCode =
+      scala.meta.internal.prettyprinters.TreeSyntax
+        .reprint(obtained)(newDialect.getOrElse(dialects.Scala213))
+        .toString
+    assertEquals(reprintedCode, expected)
+  }
+
+  check("question-mark", "type T = List[?]", "type T = List[?]")
+  check("underscore", "type T = List[_]", "type T = List[_]")
+  check(
+    "new-dialect-scala3",
+    "type T = List[_]",
+    "type T = List[?]",
+    newDialect = Some(dialects.Scala3)
+  )(
+    dialects.Scala211
+  )
+
+  check(
+    "new-dialect-scala213",
+    "type T = List[_]",
+    "type T = List[_]",
+    newDialect = Some(dialects.Scala213)
+  )(
+    dialects.Scala211
+  )
+  check(
+    "new-dialect-scala213-from-scala3",
+    "type T = List[?]",
+    "type T = List[?]",
+    newDialect = Some(dialects.Scala213)
+  )(
+    dialects.Scala3
+  )
+
+  test("no-origin") {
+    assertEquals(
+      Type.Placeholder(Type.Bounds(None, None)).toString(),
+      "_"
+    )
+  }
+}