Skip to content
Open
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
25 changes: 25 additions & 0 deletions compiler/src/dotty/tools/dotc/sbt/ExtractDependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,26 @@ private class ExtractDependenciesCollector(rec: DependencyRecorder) extends tpd.
rec.addClassDependency(parent.tpe.classSymbol, depContext)
}

// Only reference DependencyByMacroExpansion if it an be found on the classpath,
// as it was added later to the zinc.apiinfo DependencyContext enum
// e.g. pre 1.10.x sbt would throw java.lang.NoSuchFieldError errors here
lazy val allowsDependencyByMacroExpansion =
classOf[DependencyContext].getFields().exists(_.getName() == "DependencyByMacroExpansion")
Copy link
Member

@bishabosha bishabosha Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a mistake, this is using the static type (meaning field names are statically available), so this will always pass. (e.g. you made Zinc 1.10.x a library dependency)

There should be a way to dynamically test actual linked zinc version

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think getFields() still uses reflection (https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#getFields--), at the very least the check still works when I publish locally and use older sbt versions relying on older zinc (as opposed to what would happen without that check, where it would crash).

I did find just now that zinc ships with incrementalcompiler.version.properties resource file (but no utils for that, unfortunately), so we could probably read that if you prefer (also thank you for the timely reviews! and apologies for taking this long to update the PR).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tested just now and something like this:

  lazy val allowsDependencyByMacroExpansion =
    val reader = new BufferedReader(
      new InputStreamReader(getClass().getResourceAsStream("/incrementalcompiler.version.properties"), java.nio.charset.StandardCharsets.UTF_8)
    )
    val VersionRegex = "^version\\=(\\d+)\\.(\\d+)\\..*".r
    reader.lines.toList.asScala.exists {
      case VersionRegex(major, minor) if major.toInt > 1 || (major.toInt == 1 && minor.toInt >= 10) => true
      case _ => false
    }

also works. Let me know if I should change it to that


private def addMacroDependency(trees: List[Tree])(using Context): Unit =
if (allowsDependencyByMacroExpansion) {
val traverser = new TypeDependencyTraverser {
def addDependency(symbol: Symbol) =
if (!ignoreDependency(symbol)) {
val enclOrModuleClass = if (symbol.is(ModuleVal)) symbol.moduleClass else symbol.enclosingClass
assert(enclOrModuleClass.isClass, s"$enclOrModuleClass, $symbol")

rec.addClassDependency(enclOrModuleClass, DependencyByMacroExpansion)
}
}
trees.foreach(tree => traverser.traverse(tree.tpe))
}

private def depContextOf(cls: Symbol)(using Context): DependencyContext =
if cls.isLocal then LocalDependencyByInheritance
else DependencyByInheritance
Expand Down Expand Up @@ -226,6 +246,11 @@ private class ExtractDependenciesCollector(rec: DependencyRecorder) extends tpd.
case _ =>
}

tree match
case TypeApply(fun, args) if fun.symbol.is(Inline) =>
addMacroDependency(args)
case _ =>

tree match {
case tree: Inlined if !tree.inlinedFromOuterScope =>
// The inlined call is normally ignored by TreeTraverser but we need to
Expand Down
2 changes: 1 addition & 1 deletion project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1428,7 +1428,7 @@ object Build {
Compile / scalacOptions ++= Seq("--java-output-version", Versions.minimumJVMVersion),
// Add all the project's external dependencies
libraryDependencies ++= Seq(
("org.scala-sbt" %% "zinc-apiinfo" % "1.8.0" % Test).cross(CrossVersion.for3Use2_13),
("org.scala-sbt" %% "zinc-apiinfo" % "1.10.8" % Test).cross(CrossVersion.for3Use2_13),
"com.github.sbt" % "junit-interface" % "0.13.3" % Test,
),
// Exclude the transitive dependencies from `zinc-apiinfo` that causes issues at the moment
Expand Down
9 changes: 9 additions & 0 deletions sbt-test/macros/i23852/Components.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
object Components extends App {
try {
wire[Dep]
} catch {
case e: Throwable =>
e.printStackTrace()
sys.exit(-1)
}
}
1 change: 1 addition & 0 deletions sbt-test/macros/i23852/Dep.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
class Dep()
1 change: 1 addition & 0 deletions sbt-test/macros/i23852/Dep.scala-added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
class Dep(int: Int)
16 changes: 16 additions & 0 deletions sbt-test/macros/i23852/macro.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import scala.quoted.*

inline def wire[T]: T = ${ wireImpl[T] }
def wireImpl[T: Type](using q: Quotes): Expr[T] = {
import q.reflect.*

lazy val targetType = TypeRepr.of[T]
val constructorValue = targetType.typeSymbol.primaryConstructor
val constructionMethodTree: Term = {
val ctor = Select(New(TypeIdent(targetType.typeSymbol)), constructorValue)
if (targetType.typeArgs.isEmpty) ctor else ctor.appliedToTypes(targetType.typeArgs)
}
val constructorArgsValue = List(Nil)
val code: Tree = constructorArgsValue.foldLeft(constructionMethodTree)((acc: Term, args: List[Term]) => Apply(acc, args))
code.asExprOf[T]
}
11 changes: 11 additions & 0 deletions sbt-test/macros/i23852/project/DottyInjectedPlugin.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import sbt._
import Keys._

object DottyInjectedPlugin extends AutoPlugin {
override def requires = plugins.JvmPlugin
override def trigger = allRequirements

override val projectSettings = Seq(
scalaVersion := sys.props("plugin.scalaVersion")
)
}
3 changes: 3 additions & 0 deletions sbt-test/macros/i23852/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
> run
$ copy-file Dep.scala-added Dep.scala
-> compile
2 changes: 2 additions & 0 deletions sbt-test/macros/macro-type-change-2/A/A.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package A
class A
11 changes: 11 additions & 0 deletions sbt-test/macros/macro-type-change-2/app/App.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package app

import Macros.*
import A.A

object App {
@main def hasFields(expected: Boolean): Unit = {
val actual = Macros.hasAnyField[A]
assert(expected == actual, s"Expected $expected, obtained $actual")
}
}
27 changes: 27 additions & 0 deletions sbt-test/macros/macro-type-change-2/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// {
// "projects": [
// {
// "name": "app",
// "dependsOn": [
// "macros",
// "A"
// ],
// "scalaVersion": "2.13.12"
// },
// {
// "name": "macros",
// "scalaVersion": "2.13.12"
// },
// {
// "name": "A",
// "scalaVersion": "2.13.12"
// }
// ]
// }

lazy val app = project.in(file("app"))
.dependsOn(macros, A)

lazy val macros = project.in(file("macros"))

lazy val A = project.in(file("A"))
4 changes: 4 additions & 0 deletions sbt-test/macros/macro-type-change-2/changes/A1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package A
class A {
val hello: String = ""
}
15 changes: 15 additions & 0 deletions sbt-test/macros/macro-type-change-2/macros/Macros.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package Macros

import scala.quoted.*

object Macros {
inline def hasAnyField[T]: Boolean = ${ hasAnyFieldImpl[T] }

def hasAnyFieldImpl[T: Type](using Quotes): Expr[Boolean] = {
import quotes.reflect.*

val hasField = TypeRepr.of[T].typeSymbol.fieldMembers.nonEmpty

Expr(hasField)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import sbt._
import Keys._

object DottyInjectedPlugin extends AutoPlugin {
override def requires = plugins.JvmPlugin
override def trigger = allRequirements

override val projectSettings = Seq(
scalaVersion := sys.props("plugin.scalaVersion")
)
}
4 changes: 4 additions & 0 deletions sbt-test/macros/macro-type-change-2/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# adapted from https://github.com/sbt/zinc/blob/1e422e5525c698aa71cc35b30c275c8c1c3135b2/zinc/src/sbt-test/macros/macro-type-change-2/test
> app/run false
$ copy-file changes/A1.scala A/A.scala
> app/run true
2 changes: 2 additions & 0 deletions sbt-test/macros/macro-type-change-3/A/A.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package A
class A
2 changes: 2 additions & 0 deletions sbt-test/macros/macro-type-change-3/A/B.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package A
class B extends A
11 changes: 11 additions & 0 deletions sbt-test/macros/macro-type-change-3/app/App.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package app

import Macros.*
import A.B

object App {
@main def hasFields(expected: Boolean): Unit = {
val actual = Macros.hasAnyField[B]
assert(expected == actual, s"Expected $expected, obtained $actual")
}
}
27 changes: 27 additions & 0 deletions sbt-test/macros/macro-type-change-3/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// {
// "projects": [
// {
// "name": "app",
// "dependsOn": [
// "macros",
// "A"
// ],
// "scalaVersion": "2.13.12"
// },
// {
// "name": "macros",
// "scalaVersion": "2.13.12"
// },
// {
// "name": "A",
// "scalaVersion": "2.13.12"
// }
// ]
// }

lazy val app = project.in(file("app"))
.dependsOn(macros, A)

lazy val macros = project.in(file("macros"))

lazy val A = project.in(file("A"))
4 changes: 4 additions & 0 deletions sbt-test/macros/macro-type-change-3/changes/A1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package A
class A {
val hello: String = ""
}
15 changes: 15 additions & 0 deletions sbt-test/macros/macro-type-change-3/macros/Macros.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package Macros

import scala.quoted.*

object Macros {
inline def hasAnyField[T]: Boolean = ${ hasAnyFieldImpl[T] }

def hasAnyFieldImpl[T: Type](using Quotes): Expr[Boolean] = {
import quotes.reflect.*

val hasField = TypeRepr.of[T].typeSymbol.fieldMembers.nonEmpty

Expr(hasField)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import sbt._
import Keys._

object DottyInjectedPlugin extends AutoPlugin {
override def requires = plugins.JvmPlugin
override def trigger = allRequirements

override val projectSettings = Seq(
scalaVersion := sys.props("plugin.scalaVersion")
)
}
4 changes: 4 additions & 0 deletions sbt-test/macros/macro-type-change-3/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# adapted from https://github.com/sbt/zinc/blob/1e422e5525c698aa71cc35b30c275c8c1c3135b2/zinc/src/sbt-test/macros/macro-type-change-3/test
> app/run false
$ copy-file changes/A1.scala A/A.scala
> app/run true
2 changes: 2 additions & 0 deletions sbt-test/macros/macro-type-change-4/A/A.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package A
class A
11 changes: 11 additions & 0 deletions sbt-test/macros/macro-type-change-4/app/App.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package app

import Macros.*
import A.A

object App {
@main def hasFields(expected: Boolean): Unit = {
val actual = Macros.hasAnyField[A](true)
assert(expected == actual, s"Expected $expected, obtained $actual")
}
}
27 changes: 27 additions & 0 deletions sbt-test/macros/macro-type-change-4/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// {
// "projects": [
// {
// "name": "app",
// "dependsOn": [
// "macros",
// "A"
// ],
// "scalaVersion": "2.13.12"
// },
// {
// "name": "macros",
// "scalaVersion": "2.13.12"
// },
// {
// "name": "A",
// "scalaVersion": "2.13.12"
// }
// ]
// }

lazy val app = project.in(file("app"))
.dependsOn(macros, A)

lazy val macros = project.in(file("macros"))

lazy val A = project.in(file("A"))
4 changes: 4 additions & 0 deletions sbt-test/macros/macro-type-change-4/changes/A1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package A
class A {
val hello: String = ""
}
15 changes: 15 additions & 0 deletions sbt-test/macros/macro-type-change-4/macros/Macros.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package Macros

import scala.quoted.*

object Macros {
inline def hasAnyField[T](placeholder: Boolean): Boolean = ${ hasAnyFieldImpl[T]('placeholder) }

def hasAnyFieldImpl[T: Type](placeholder: Expr[Boolean])(using Quotes): Expr[Boolean] = {
import quotes.reflect.*

val hasField = TypeRepr.of[T].typeSymbol.fieldMembers.nonEmpty

Expr(hasField)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import sbt._
import Keys._

object DottyInjectedPlugin extends AutoPlugin {
override def requires = plugins.JvmPlugin
override def trigger = allRequirements

override val projectSettings = Seq(
scalaVersion := sys.props("plugin.scalaVersion")
)
}
4 changes: 4 additions & 0 deletions sbt-test/macros/macro-type-change-4/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# adapted from https://github.com/sbt/zinc/blob/1e422e5525c698aa71cc35b30c275c8c1c3135b2/zinc/src/sbt-test/macros/macro-type-change-4/test
> app/run false
$ copy-file changes/A1.scala A/A.scala
> app/run true
2 changes: 2 additions & 0 deletions sbt-test/macros/macro-type-change/app/A.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package app
class A
10 changes: 10 additions & 0 deletions sbt-test/macros/macro-type-change/app/App.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package app

import Macros.*

object App {
@main def hasFields(expected: Boolean): Unit = {
val actual = Macros.hasAnyField[A]
assert(expected == actual, s"Expected $expected, obtained $actual")
}
}
20 changes: 20 additions & 0 deletions sbt-test/macros/macro-type-change/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// {
// "projects": [
// {
// "name": "app",
// "dependsOn": [
// "macros"
// ],
// "scalaVersion": "2.13.12"
// },
// {
// "name": "macros",
// "scalaVersion": "2.13.12"
// }
// ]
// }

lazy val app = project.in(file("app"))
.dependsOn(macros)

lazy val macros = project.in(file("macros"))
4 changes: 4 additions & 0 deletions sbt-test/macros/macro-type-change/changes/A1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package app
class A {
val hello: String = ""
}
15 changes: 15 additions & 0 deletions sbt-test/macros/macro-type-change/macros/Macros.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package Macros

import scala.quoted.*

object Macros {
inline def hasAnyField[T]: Boolean = ${ hasAnyFieldImpl[T] }

def hasAnyFieldImpl[T: Type](using Quotes): Expr[Boolean] = {
import quotes.reflect.*

val hasField = TypeRepr.of[T].typeSymbol.fieldMembers.nonEmpty

Expr(hasField)
}
}
Loading
Loading