Repository: scalameta/scalameta (scala)

Issue:
Export clause with syntax "ClassName.this.fieldName" fails to parse
Hi, I noticed this error when using Metals, but I assume this is a scalameta issue?

scalameta reports this parse error for the code below: \`.\` expected but \`outdent\` found


```Scala
case class Test():
    val name = "Test"

    case class Test2():
        export Test.this.name

@main def main(): Unit =
    val t = Test()
    val t2 = t.Test2()
    
    println(t.name)
    println(t2.name)
```

Even though it's a valid code that compiles and runs:

```
sbt:test_export> compile
[info] compiling 1 Scala source to /Users/user/tmp/test_export/target/scala-3.4.2/classes ...
[success] Total time: 2 s, completed 27 Jun 2024, 14.46.04
sbt:test_export> run
[info] running main
Test
Test
[success] Total time: 0 s, completed 27 Jun 2024, 14.46.06
```

Screenshot of the error:
<img width="327" alt="Screenshot 2024-06-27 at 14 58 15" src="https://github.com/scalameta/scalameta/assets/41424335/0c1924ed-f14d-46be-98e9-1605877e91be">

Pull request:
The latter also includes `Term.This`. Fixes #3754.

Patch:
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala
index ca9296d276..85d30144a4 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala
@@ -3192,7 +3192,7 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) {
     def dotselectors = Importer(sid, importees())
     def name(tn: Term.Name) = copyPos(tn)(Name.Indeterminate(tn.value))
     sid match {
-      case Term.Select(sid: Term.Ref, tn: Term.Name) if sid.isStableId =>
+      case Term.Select(sid: Term.Ref, tn: Term.Name) if sid.isPath =>
         if (acceptOpt[Dot]) dotselectors
         else if (acceptOpt(soft.KwAs(_))) Importer(sid, importeeRename(name(tn)) :: Nil)
         else if (Wildcard.isStar(tn.tokens.head))
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala b/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala
index 53effb6c0b..55dbe3bd0d 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala
@@ -1234,7 +1234,7 @@ class Export(importers: List[Importer] @nonEmpty) extends ImportExportStat
 
 @ast
 class Importer(ref: Term.Ref, importees: List[Importee] @nonEmpty) extends Tree {
-  checkFields(ref.isStableId)
+  checkFields(ref.isPath)
 }
 
 @branch
diff --git a/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/package.scala b/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/package.scala
index bbd1202b00..99857548ab 100644
--- a/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/package.scala
+++ b/scalameta/trees/shared/src/main/scala/scala/meta/internal/trees/package.scala
@@ -80,26 +80,27 @@ package object trees {
   }
 
   implicit class XtensionTreesTerm(private val tree: Term) extends AnyVal {
+    @tailrec
     def isExtractor: Boolean = tree match {
       case quasi: Term.Quasi => true
-      case ref: Term.Ref => ref.isStableId
+      case ref: Term.Ref => ref.isPath
       case t: Term.ApplyType => t.fun.isExtractor
       case _ => false
     }
   }
 
   implicit class XtensionTreesTermRef(private val tree: Term.Ref) extends AnyVal {
-    def isPath: Boolean = tree.isStableId || tree.is[Term.This]
+    @tailrec
     def isQualId: Boolean = tree match {
       case _: Term.Ref.Quasi => true
       case _: Term.Name => true
       case Term.Select(qual: Term.Ref, _) => qual.isQualId
       case _ => false
     }
-    def isStableId: Boolean = tree match {
-      case _: Term.Ref.Quasi => true
-      case _: Term.Name | _: Term.Anonymous | Term.Select(_: Term.Super, _) => true
-      case Term.Select(qual: Term.Quasi, _) => true
+    @tailrec
+    def isPath: Boolean = tree match {
+      case _: Term.Ref.Quasi | _: Term.This | _: Term.Name | _: Term.Anonymous => true
+      case Term.Select(_: Term.Super | _: Term.Quasi, _) => true
       case Term.Select(qual: Term.Ref, _) => qual.isPath
       case _ => false
     }


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ExportImportSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ExportImportSuite.scala
index 0506f6c5ce..1b2e3d64e8 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ExportImportSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/ExportImportSuite.scala
@@ -104,4 +104,20 @@ class ExportImportSuite extends BaseDottySuite {
       List(Importer(Term.Select(tname("A"), tname("b")), List(Importee.Name(Name("*")))))
     ))
   }
+
+  test("#3754 export Type.this.field") {
+    val code = """|case class A():
+                  |  export A.this.value
+                  |""".stripMargin
+    val layout = "case class A() { export A.this.value }"
+    val tree = Defn.Class(
+      List(Mod.Case()),
+      pname("A"),
+      Nil,
+      ctorp(Nil),
+      tpl(Export(List(Importer(Term.This(Name("A")), List(Importee.Name(Name("value")))))))
+    )
+    runTestAssert[Stat](code, layout)(tree)
+  }
+
 }