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

Backport "Fix #20471: owners of top-level symbols in cached quoted code being incorrect" to 3.3 LTS #103

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/quoted/PickledQuotes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,9 @@ object PickledQuotes {
treeOwner(tree) match
case Some(owner) =>
// Copy the cached tree to make sure the all definitions are unique.
TreeTypeMap(oldOwners = List(owner), newOwners = List(owner)).apply(tree)
val treeCpy = TreeTypeMap(oldOwners = List(owner), newOwners = List(owner)).apply(tree)
// Then replace the symbol owner with the one pointed by the quote context.
treeCpy.changeNonLocalOwners(ctx.owner)
case _ =>
tree

Expand Down
9 changes: 9 additions & 0 deletions sbt-test/sbt-dotty/scaladoc-regressions/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ThisBuild / scalaVersion := sys.props("plugin.scalaVersion")

lazy val i20476 = project
.in(file("i20476"))
.enablePlugins(ScalaJSPlugin)

lazy val i18231 = project
.in(file("i18231"))
.settings(scalacOptions += "-release:8")
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
object Foo {
@Deprecated
def foo(): Unit = ???
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package demo

import scala.scalajs.js

def bar: js.Promise[Int] = js.Promise.resolve(()).`then`(_ => 1)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("org.scala-js" % "sbt-scalajs" % sys.props("plugin.scalaJSVersion"))
2 changes: 2 additions & 0 deletions sbt-test/sbt-dotty/scaladoc-regressions/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
> i18231/doc
> i20476/doc
4 changes: 2 additions & 2 deletions scaladoc/src/dotty/tools/scaladoc/tasty/TastyParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package tasty
import java.util.regex.Pattern

import scala.util.{Try, Success, Failure}
import scala.tasty.inspector.{TastyInspector, Inspector, Tasty}
import scala.tasty.inspector.{ScaladocInternalTastyInspector, Inspector, Tasty}
import scala.quoted._

import dotty.tools.dotc
Expand Down Expand Up @@ -160,7 +160,7 @@ object ScaladocTastyInspector:
report.error("File extension is not `tasty` or `jar`: " + invalidPath)

if tastyPaths.nonEmpty then
TastyInspector.inspectAllTastyFiles(tastyPaths, jarPaths, classpath)(inspector)
ScaladocInternalTastyInspector.inspectAllTastyFilesInContext(tastyPaths, jarPaths, classpath)(inspector)(using ctx.compilerContext)

val all = inspector.topLevels.result()
all.groupBy(_._1).map { case (pckName, members) =>
Expand Down
43 changes: 32 additions & 11 deletions scaladoc/src/scala/tasty/inspector/TastyInspector.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copy of tasty-inspector/src/scala/tasty/inspector/TastyInspector.scala
// Renamed copy of tasty-inspector/src/scala/tasty/inspector/TastyInspector.scala
// FIXME remove this copy of the file
// Since copying, an inspectAllTastyFilesInContext method was added for scaladoc only
// to fix regressions introduced by the switch from old to a new TastyInspector

package scala.tasty.inspector

Expand All @@ -21,7 +23,7 @@ import dotty.tools.dotc.report

import java.io.File.pathSeparator

object TastyInspector:
object ScaladocInternalTastyInspector:

/** Load and process TASTy files using TASTy reflect
*
Expand All @@ -41,6 +43,32 @@ object TastyInspector:
def inspectTastyFilesInJar(jar: String)(inspector: Inspector): Boolean =
inspectAllTastyFiles(Nil, List(jar), Nil)(inspector)

private def checkFiles(tastyFiles: List[String], jars: List[String]): Unit =
def checkFile(fileName: String, ext: String): Unit =
val file = dotty.tools.io.Path(fileName)
if !file.ext.toLowerCase.equalsIgnoreCase(ext) then
throw new IllegalArgumentException(s"File extension is not `.$ext`: $file")
else if !file.exists then
throw new IllegalArgumentException(s"File not found: ${file.toAbsolute}")
tastyFiles.foreach(checkFile(_, "tasty"))
jars.foreach(checkFile(_, "jar"))

/**
* Added for Scaladoc-only.
* Meant to fix regressions introduces by the switch from old to new TastyInspector:
* https://github.com/scala/scala3/issues/18231
* https://github.com/scala/scala3/issues/20476
* Stable TastyInspector API does not support passing compiler context.
*/
def inspectAllTastyFilesInContext(tastyFiles: List[String], jars: List[String], dependenciesClasspath: List[String])(inspector: Inspector)(using Context): Boolean =
checkFiles(tastyFiles, jars)
val classes = tastyFiles ::: jars
classes match
case Nil => true
case _ =>
val reporter = inspectorDriver(inspector).process(inspectorArgs(dependenciesClasspath, classes), summon[Context])
!reporter.hasErrors

/** Load and process TASTy files using TASTy reflect
*
* @param tastyFiles List of paths of `.tasty` files
Expand All @@ -50,14 +78,7 @@ object TastyInspector:
* @return boolean value indicating whether the process succeeded
*/
def inspectAllTastyFiles(tastyFiles: List[String], jars: List[String], dependenciesClasspath: List[String])(inspector: Inspector): Boolean =
def checkFile(fileName: String, ext: String): Unit =
val file = dotty.tools.io.Path(fileName)
if file.extension != ext then
throw new IllegalArgumentException(s"File extension is not `.$ext`: $file")
else if !file.exists then
throw new IllegalArgumentException(s"File not found: ${file.toAbsolute}")
tastyFiles.foreach(checkFile(_, "tasty"))
jars.foreach(checkFile(_, "jar"))
checkFiles(tastyFiles, jars)
val files = tastyFiles ::: jars
inspectFiles(dependenciesClasspath, files)(inspector)

Expand Down Expand Up @@ -124,4 +145,4 @@ object TastyInspector:
end inspectFiles


end TastyInspector
end ScaladocInternalTastyInspector
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ class Scaladoc3ExternalLocationProviderIntegrationTest extends ExternalLocationP

def getScalaLibraryPath: String = {
val classpath: List[String] = System.getProperty("java.class.path").split(java.io.File.pathSeparatorChar).toList
val stdlib = classpath.find(_.contains("scala-library-2")).getOrElse("foobarbazz") // If we don't find the scala 2 library, the test will fail
new java.io.File(stdlib).getCanonicalPath() // canonicalize for case-insensitive file systems
// For an unclear reason, depending on if we pass the compiler context onto the tasty inspector
// the scala-2-library path needs to have its characters case fixed with new java.io.File(stdlib).getCanonicalPath()
classpath.find(_.contains("scala-library-2")).getOrElse("foobarbazz") // If we don't find the scala 2 library, the test will fail
}

class Scaladoc2LegacyExternalLocationProviderIntegrationTest extends LegacyExternalLocationProviderIntegrationTest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class LinkWarningsTest extends ScaladocTest("noLinkWarnings"):

override def runTest = afterRendering {
val diagnostics = summon[DocContext].compilerContext.reportedDiagnostics
assertEquals("There should be exactly one warning", 1, diagnostics.warningMsgs.size)
val filteredWarnings = diagnostics.warningMsgs.filter(_ != "1 warning found")
assertEquals("There should be exactly one warning", 1, filteredWarnings.size)
assertNoErrors(diagnostics)
}
63 changes: 63 additions & 0 deletions tests/pos-macros/i20471/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import scala.annotation.experimental
import scala.quoted.*
import scala.annotation.tailrec

object FlatMap {
@experimental inline def derived[F[_]]: FlatMap[F] = MacroFlatMap.derive
}
trait FlatMap[F[_]]{
def tailRecM[A, B](a: A)(f: A => F[Either[A, B]]): F[B]
}

@experimental
object MacroFlatMap:

inline def derive[F[_]]: FlatMap[F] = ${ flatMap }

def flatMap[F[_]: Type](using Quotes): Expr[FlatMap[F]] = '{
new FlatMap[F]:
def tailRecM[A, B](a: A)(f: A => F[Either[A, B]]): F[B] =
${ deriveTailRecM('{ a }, '{ f }) }
}

def deriveTailRecM[F[_]: Type, A: Type, B: Type](
a: Expr[A],
f: Expr[A => F[Either[A, B]]]
)(using q: Quotes): Expr[F[B]] =
import quotes.reflect.*

val body: PartialFunction[(Symbol, TypeRepr), Term] = {
case (method, tpe) => {
given q2: Quotes = method.asQuotes
'{
def step(x: A): B = ???
???
}.asTerm
}
}

val term = '{ $f($a) }.asTerm
val name = Symbol.freshName("$anon")
val parents = List(TypeTree.of[Object], TypeTree.of[F[B]])

extension (sym: Symbol) def overridableMembers: List[Symbol] =
val member1 = sym.methodMember("abstractEffect")(0)
val member2 = sym.methodMember("concreteEffect")(0)
def meth(member: Symbol) = Symbol.newMethod(sym, member.name, This(sym).tpe.memberType(member), Flags.Override, Symbol.noSymbol)
List(meth(member1), meth(member2))

val cls = Symbol.newClass(Symbol.spliceOwner, name, parents.map(_.tpe), _.overridableMembers, None)

def transformDef(method: DefDef)(argss: List[List[Tree]]): Option[Term] =
val sym = method.symbol
Some(body.apply((sym, method.returnTpt.tpe)))

val members = cls.declarations
.filterNot(_.isClassConstructor)
.map: sym =>
sym.tree match
case method: DefDef => DefDef(sym, transformDef(method))
case _ => report.errorAndAbort(s"Not supported: $sym in ${sym.owner}")

val newCls = New(TypeIdent(cls)).select(cls.primaryConstructor).appliedToNone
Block(ClassDef(cls, parents, members) :: Nil, newCls).asExprOf[F[B]]
7 changes: 7 additions & 0 deletions tests/pos-macros/i20471/Main_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import scala.annotation.experimental

@experimental
object autoFlatMapTests:
trait TestAlgebra[T] derives FlatMap:
def abstractEffect(a: String): T
def concreteEffect(a: String): T = abstractEffect(a + " concreteEffect")