Repository: scalameta/scalameta (scala)

Issue:
Cannot format scala3 macro quote block if there are white-space after `'`
Cannot format scala3 macro quote block if there are white-space between `'` and `{`

For example

```scala
'  {
xxx
}
```
will complain `error: unclosed character literal`

Pull request:
Fixes https://github.com/scalameta/scalameta/issues/2482

Patch:
diff --git a/scalameta/tokenizers/shared/src/main/scala/scala/meta/internal/tokenizers/CharArrayReader.scala b/scalameta/tokenizers/shared/src/main/scala/scala/meta/internal/tokenizers/CharArrayReader.scala
index 6dd7f1f4ba..cff50aa242 100644
--- a/scalameta/tokenizers/shared/src/main/scala/scala/meta/internal/tokenizers/CharArrayReader.scala
+++ b/scalameta/tokenizers/shared/src/main/scala/scala/meta/internal/tokenizers/CharArrayReader.scala
@@ -73,6 +73,11 @@ class CharArrayReader(input: Input, dialect: Dialect, reporter: Reporter)
     }
   }
 
+  def nextNonWhitespace = {
+    while (ch == ' ' || ch == '\t') nextRawChar()
+    ch
+  }
+
   /** Interpret \\uxxxx escapes */
   private def potentialUnicode() = {
     def evenSlashPrefix: Boolean = {
diff --git a/scalameta/tokenizers/shared/src/main/scala/scala/meta/internal/tokenizers/LegacyScanner.scala b/scalameta/tokenizers/shared/src/main/scala/scala/meta/internal/tokenizers/LegacyScanner.scala
index 8f6735235f..8c2d6d2821 100644
--- a/scalameta/tokenizers/shared/src/main/scala/scala/meta/internal/tokenizers/LegacyScanner.scala
+++ b/scalameta/tokenizers/shared/src/main/scala/scala/meta/internal/tokenizers/LegacyScanner.scala
@@ -345,7 +345,11 @@ class LegacyScanner(input: Input, dialect: Dialect) {
           val prevChar = ch
           putChar(ch)
           nextChar()
-          if (prevChar == '$' && ch == '{' && dialect.allowSpliceAndQuote) {
+          def nextIsLeftBrace = {
+            val lookahead = lookaheadReader
+            lookahead.nextNonWhitespace == '{'
+          }
+          if (prevChar == '$' && dialect.allowSpliceAndQuote && nextIsLeftBrace) {
             token = MACROSPLICE
             setStrVal()
           } else {
@@ -454,9 +458,13 @@ class LegacyScanner(input: Input, dialect: Dialect) {
           else if (isOperatorPart(ch) && (ch != '\\' || isUnicodeEscape))
             charLitOr(getOperatorRest _)
           else {
-            val lookahead = lookaheadReader
-            lookahead.nextRawChar()
-            if ((ch == '{' || ch == '[') && lookahead.ch != '\'' && dialect.allowSpliceAndQuote) {
+            def isNotBraceOrBracketLiteral = {
+              val lookahead = lookaheadReader
+              val nextNonWhitespace = lookahead.nextNonWhitespace
+              lookahead.nextRawChar()
+              (nextNonWhitespace == '{' || nextNonWhitespace == '[') && lookahead.ch != '\''
+            }
+            if (dialect.allowSpliceAndQuote && isNotBraceOrBracketLiteral) {
               token = MACROQUOTE
               setStrVal()
             } else {


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MacroSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MacroSuite.scala
index 8c60e91102..dad21f0997 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MacroSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/MacroSuite.scala
@@ -9,13 +9,13 @@ class MacroSuite extends BaseDottySuite {
 
   /**
    * All examples based on dotty documentation:
-   *  https://dotty.epfl.ch/docs/reference/metaprogramming/toc.html
-   *  https://dotty.epfl.ch/docs/reference/metaprogramming/macros.html
-   *  https://dotty.epfl.ch/docs/reference/metaprogramming/tasty-reflect.html
+   * https://dotty.epfl.ch/docs/reference/metaprogramming/toc.html
+   * https://dotty.epfl.ch/docs/reference/metaprogramming/macros.html
+   * https://dotty.epfl.ch/docs/reference/metaprogramming/tasty-reflect.html
    *
-   *  '{ ... } OR 'ident - QuotedMacroExpr
-   *  '[ ... ] - QuotedMacroType
-   *  ${ ... } OR $ident - SplicedMacroExpr
+   * - '{ ... } OR 'ident - QuotedMacroExpr
+   * - '[ ... ] - QuotedMacroType
+   * - ${ ... } OR $ident -  SplicedMacroExpr
    */
   test("parse-single-quote-character") {
     runTestAssert[Stat]("val a = 'c'")(
@@ -23,6 +23,12 @@ class MacroSuite extends BaseDottySuite {
     )
   }
 
+  test("parse-single-quote-space") {
+    runTestAssert[Stat]("' { 'ax }", assertLayout = Some("'{ 'ax }"))(
+      Term.QuotedMacroExpr(Term.Block(List(Term.QuotedMacroExpr(tname("ax")))))
+    )
+  }
+
   test("macro-quote-expr") {
     runTestAssert[Stat]("'{ 'ax }")(
       Term.QuotedMacroExpr(Term.Block(List(Term.QuotedMacroExpr(tname("ax")))))
@@ -180,6 +186,9 @@ class MacroSuite extends BaseDottySuite {
     runTestAssert[Stat]("${ $x }")(
       Term.SplicedMacroExpr(Term.Block(List(Term.SplicedMacroExpr(tname("x")))))
     )
+    runTestAssert[Stat]("$ { $x }", assertLayout = Some("${ $x }"))(
+      Term.SplicedMacroExpr(Term.Block(List(Term.SplicedMacroExpr(tname("x")))))
+    )
     runTestAssert[Stat]("${ powerCode('x) }")(
       Term.SplicedMacroExpr(
         Term.Block(List(Term.Apply(tname("powerCode"), List(Term.QuotedMacroExpr(tname("x"))))))
diff --git a/tests/shared/src/test/scala/scala/meta/tests/tokenizers/TokenizerSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/tokenizers/TokenizerSuite.scala
index ce4468dd68..954e9e5883 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/tokenizers/TokenizerSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/tokenizers/TokenizerSuite.scala
@@ -979,6 +979,34 @@ class TokenizerSuite extends BaseTokenizerSuite {
     assert(dialects.Dotty("s\"$enum\"").tokenize.isInstanceOf[Tokenized.Error])
   }
 
+  test("macro") {
+    val Tokens(
+      BOF(),
+      MacroQuote(),
+      Space(),
+      LeftBrace(),
+      Space(),
+      Ident("a"),
+      Space(),
+      RightBrace(),
+      EOF()
+    ) =
+      dialects.Scala3("' { a }").tokenize.get
+
+    val Tokens(
+      BOF(),
+      MacroSplice(),
+      Space(),
+      LeftBrace(),
+      Space(),
+      Ident("a"),
+      Space(),
+      RightBrace(),
+      EOF()
+    ) =
+      dialects.Scala3("$ { a }").tokenize.get
+  }
+
   test("numeric literal separator") {
     dialects.Scala213("1_024").tokenize.get
     dialects.Scala213("1_024L").tokenize.get