Repository: scalameta/scalameta (scala)

Issue:
Code expressions in Scaladoc parsing
The Scaladoc parser expects code expressions to be formatted as `{{{x + y}}}`. There are two problems with that:

- The current Scaladoc generates a _code block_ from it, even when `{{{` is not followed by a newline.
- The Scaladoc documentation suggests _not_ to use wikidoc in new projects but to switch to markdown instead.

So, `{{{x + y}}}` should really be `` `x + y` `` in new code, but this is _not_ parsed as a `CodeExpr` token by Scalameta. This causes problems in tools that rely on Scalameta, such as Scalafmt (see <https://github.com/scalameta/scalafmt/issues/4147>).

I think `` `x + y` `` should be parsed as a new `MdCodeExpr` that extends `TextPart` like `CodeExpr` (there is already `MdCodeBlock` as an alternate to `CodeBlock`).

Pull request description:
Fixes #3908.

Patch:
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScaladocParser.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScaladocParser.scala
index 2235d64751..61d3690e4b 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScaladocParser.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScaladocParser.scala
@@ -93,6 +93,15 @@ object ScaladocParser {
     }
   }
 
+  private def mdCodeSpanParser[$: P]: P[MdCodeSpan] = P {
+    val tick = "`"
+    tick.rep(1).!.flatMap { fence =>
+      def end = fence ~ !tick
+      def expr = (!end ~ !nl ~ AnyChar).rep.! ~ end ~ punctParser.!
+      expr.map { case (code, punct) => MdCodeSpan(code, fence, punct) }
+    }
+  }
+
   private def headingParser[$: P]: P[Heading] = P {
     // heading delimiter
     hspaces0 ~ CharsWhileIn("=", 1).!.flatMap { delim =>
@@ -125,7 +134,8 @@ object ScaladocParser {
 
   private def textParser[$: P](indent: Int, mdOffset: Int = 0): P[Text] = P {
     def end = P(nl ~/ nextPartParser(indent, mdOffset))
-    def part: P[TextPart] = P(codeExprParser | linkParser | enclosedJavaTagParser | wordParser)
+    def part: P[TextPart] =
+      P(codeExprParser | mdCodeSpanParser | linkParser | enclosedJavaTagParser | wordParser)
     def sep = P(!end ~ nl.? ~ hspaces0)
     hspaces0 ~ part.rep(1, sep = sep).map(x => Text(x))
   }
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/internal/Scaladoc.scala b/scalameta/trees/shared/src/main/scala/scala/meta/internal/Scaladoc.scala
index d78f3ffcec..b6f75ac97a 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/internal/Scaladoc.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/internal/Scaladoc.scala
@@ -47,6 +47,11 @@ object Scaladoc {
     override def syntax: String = s"{{{$code}}}$punct"
   }
 
+  /** A markdown code span, an embedded code expression */
+  final case class MdCodeSpan(code: String, fence: String, punct: String = "") extends TextPart {
+    override def syntax: String = s"$fence$code$fence$punct"
+  }
+
   /** Represents an enclosed tagged documentation remark */
   final case class EnclosedJavaTag(tag: String, desc: Seq[String] = Nil) extends TextPart {
     override def syntax: String = (tag +: desc).mkString("{", " ", "}")


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/ScaladocParserSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/ScaladocParserSuite.scala
index 3539d15d14..893699a2a3 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/ScaladocParserSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/ScaladocParserSuite.scala
@@ -343,6 +343,62 @@ class ScaladocParserSuite extends FunSuite {
     assertEquals(result, expectation)
   }
 
+  test("markdown code span 1") {
+    val result = parseString(
+      s"""
+          /**   foo `` bar ` ` baz ``, and
+            *   qux `quux xyzzy`
+            */
+       """.stripMargin
+    )
+
+    val expectation = Some(Scaladoc(Seq(Paragraph(Seq(Text(Seq(
+      Word("foo"),
+      MdCodeSpan(" bar ` ` baz ", "``", ","),
+      Word("and"),
+      Word("qux"),
+      MdCodeSpan("quux xyzzy", "`", "")
+    )))))))
+
+    assertEquals(result, expectation)
+  }
+
+  test("pseudo-markdown code span within list") {
+    val result = parseString(
+      s"""
+          /**   - foo `bar
+            *   - baz qux`
+            */
+       """.stripMargin
+    )
+
+    val expectation = Some(Scaladoc(Seq(Paragraph(Seq(ListBlock(
+      "-",
+      Seq(
+        ListItem(Text(Seq(Word("foo"), Word("`bar"))), Nil),
+        ListItem(Text(Seq(Word("baz"), Word("qux`"))), Nil)
+      )
+    ))))))
+
+    assertEquals(result, expectation)
+  }
+
+  test("markdown code span vs code block") {
+    val result = parseString(
+      s"""
+          /**   ``` foo ` ` bar ```
+            *   ```foo
+            *   ```
+            */
+       """.stripMargin
+    )
+
+    val expectation = Some(Scaladoc(Seq(Paragraph(
+      Seq(Text(Seq(MdCodeSpan(" foo ` ` bar ", "```", ""))), MdCodeBlock(Seq("foo"), Nil, "```"))
+    ))))
+    assertEquals(result, expectation)
+  }
+
   test("headings") {
     val level1HeadingBody = "Level 1"
     val level2HeadingBody = "Level 2"