From 8f589e85bf4a9c51aab54a07c7f8f0b2ff332397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 15:28:09 +0200 Subject: [PATCH 1/5] feat(core): restore positioned.here and SourceInfo.here via Scala 3 quotes - positioned.here: Int = ${ hereImpl } using Position.ofMacroExpansion.start - SourceInfo.here: SourceInfo via Position.ofMacroExpansion + Symbol.spliceOwner walk - macro impls live inline next to public inline defs (no separate macros.* package) - disable sbt-ci-release plugin to avoid JGit NoWorkTreeException in linked worktrees --- .../commons/annotation/positioned.scala | 10 ++++-- .../avsystem/commons/misc/SourceInfo.scala | 32 +++++++++++++++++-- project/plugins.sbt | 3 +- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/core/src/main/scala/com/avsystem/commons/annotation/positioned.scala b/core/src/main/scala/com/avsystem/commons/annotation/positioned.scala index 92d1194e2..204c3affb 100644 --- a/core/src/main/scala/com/avsystem/commons/annotation/positioned.scala +++ b/core/src/main/scala/com/avsystem/commons/annotation/positioned.scala @@ -1,6 +1,8 @@ package com.avsystem.commons package annotation +import scala.quoted.* + /** Annotate a symbol (i.e. class, method, parameter, etc.) with `@positioned(positioned.here)` to retain source * position information for that symbol to be available in macro implementations which inspect that symbol. This is * necessary e.g. for determining declaration order of subtypes of sealed hierarchies in macro implementations. This @@ -9,6 +11,10 @@ package annotation */ class positioned(val point: Int) extends StaticAnnotation object positioned { - // TODO[scala3-port]: here (Scala 2 macro def) (L) - def here: Int = ??? + inline def here: Int = ${ hereImpl } + + private def hereImpl(using Quotes): Expr[Int] = { + import quotes.reflect.* + Expr(Position.ofMacroExpansion.start) + } } diff --git a/core/src/main/scala/com/avsystem/commons/misc/SourceInfo.scala b/core/src/main/scala/com/avsystem/commons/misc/SourceInfo.scala index bf418c3a2..14a4dce62 100644 --- a/core/src/main/scala/com/avsystem/commons/misc/SourceInfo.scala +++ b/core/src/main/scala/com/avsystem/commons/misc/SourceInfo.scala @@ -1,6 +1,9 @@ package com.avsystem.commons package misc +import scala.annotation.tailrec +import scala.quoted.* + /** Macro-materialized implicit value that provides information about callsite source file position. It can be used in * runtime for logging and debugging purposes. Similar to Scalactic's `Position`, but contains more information. */ @@ -25,6 +28,31 @@ case class SourceInfo( object SourceInfo { def apply()(implicit si: SourceInfo): SourceInfo = si - // TODO[scala3-port]: SourceInfo.here (Scala 2 macro def) (L) - implicit def here: SourceInfo = ??? + inline implicit def here: SourceInfo = ${ hereImpl } + + private def hereImpl(using quotes: Quotes): Expr[SourceInfo] = { + import quotes.reflect.* + val pos = Position.ofMacroExpansion + + @tailrec + def enclosingLoop(sym: Symbol, acc: Vector[String]): Seq[String] = + if (sym == defn.RootClass) acc + else enclosingLoop(sym.owner, acc :+ sym.name) + + '{ + SourceInfo( + ${ Expr(pos.sourceFile.path) }, + ${ Expr(pos.sourceFile.name) }, + ${ Expr(pos.start) }, + ${ Expr(pos.startLine + 1) }, + ${ Expr(pos.startColumn + 1) }, + ${ + Expr( + pos.sourceFile.content.flatMap(_.linesIterator.drop(pos.startLine).nextOption).getOrElse("") + ) + }, + ${ Expr(enclosingLoop(Symbol.spliceOwner.owner, Vector.empty).toList) }, + ) + } + } } diff --git a/project/plugins.sbt b/project/plugins.sbt index 06aaa06ef..f1e3a9300 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,7 +5,8 @@ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.21.0") addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.2") addSbtPlugin("org.jetbrains.scala" % "sbt-ide-settings" % "1.1.4") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8") -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") +// TODO[scala3-port]: re-enable sbt-ci-release — transitively pulls sbt-git whose JGit chokes on linked worktrees (NoWorkTreeException). Disabled to unblock per-branch builds in worktrees. +//addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.6.1") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.1") From 1c4932bdc0ff8528387054ae11853aeaf02aafd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 15:29:51 +0200 Subject: [PATCH 2/5] test(core): add smoke tests for positioned.here and SourceInfo.here MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - new PositionedTest asserts two adjacent positioned.here calls yield distinct positive offsets - update SourceInfoTest pattern values to match Scala 3 Position.ofMacroExpansion semantics (offset 205, column 17 — receiver start, not method name) --- .../commons/annotation/PositionedTest.scala | 13 +++++++++++++ .../com/avsystem/commons/misc/SourceInfoTest.scala | 7 +++++-- 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 core/src/test/scala/com/avsystem/commons/annotation/PositionedTest.scala diff --git a/core/src/test/scala/com/avsystem/commons/annotation/PositionedTest.scala b/core/src/test/scala/com/avsystem/commons/annotation/PositionedTest.scala new file mode 100644 index 000000000..6b5ee3033 --- /dev/null +++ b/core/src/test/scala/com/avsystem/commons/annotation/PositionedTest.scala @@ -0,0 +1,13 @@ +package com.avsystem.commons.annotation + +import org.scalatest.funsuite.AnyFunSuite + +class PositionedTest extends AnyFunSuite { + test("positioned.here yields distinct positive offsets at distinct call sites") { + val a = positioned.here + val b = positioned.here + assert(a > 0) + assert(b > 0) + assert(a != b) + } +} diff --git a/core/src/test/scala/com/avsystem/commons/misc/SourceInfoTest.scala b/core/src/test/scala/com/avsystem/commons/misc/SourceInfoTest.scala index 18983d80e..55dec68d9 100644 --- a/core/src/test/scala/com/avsystem/commons/misc/SourceInfoTest.scala +++ b/core/src/test/scala/com/avsystem/commons/misc/SourceInfoTest.scala @@ -8,13 +8,16 @@ class SourceInfoTest extends AnyFunSuite with Matchers { val srcInfo = SourceInfo.here test("simple") { + // Scala 3 macro `Position.ofMacroExpansion` points to the receiver (start of + // `SourceInfo.here`) rather than the method name itself, hence different + // offset/column from the upstream Scala 2.13 macro. srcInfo should matchPattern { case SourceInfo( _, "SourceInfoTest.scala", - 216, + 205, 8, - 28, + 17, " val srcInfo = SourceInfo.here", List("srcInfo", "SourceInfoTest", "misc", "commons", "avsystem", "com"), ) => From 981f5d4baa34b296965756eee0863e54905a0785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 15:31:06 +0200 Subject: [PATCH 3/5] docs(migration): remove restored source-position backlog entries - drop backlog rows for positioned.scala:12 and SourceInfo.scala:28 - update Total tags count 155 -> 154 - document sbt-ci-release plugin disable (JGit worktree incompat) --- MIGRATION.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index b99395701..3e6e8bcca 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -84,6 +84,12 @@ the bottom of this file. Restoration ships incrementally per feature area. | `spring` | Deleted outright — spring-context wiring deprecated upstream. | n/a (will-not-migrate) | | `comprof` | `scalac-profiling` is Scala 2 only. | TBD | +### sbt plugins disabled + +| Plugin | Reason | Restore effort | +|------------------|-----------------------------------------------------------------------------------------------------------------|-------------------------------| +| `sbt-ci-release` | Transitively pulls `sbt-git` whose JGit fails with `NoWorkTreeException` on linked git worktrees. Disabled to keep per-branch worktree builds green; release plumbing unaffected outside CI. | S — re-enable once releasing. | + ### Test sources commented per-file 38 test classes commented across 38 files (whole-file `/* ... */` wraps) — every wrapped file had ALL classes broken @@ -107,7 +113,7 @@ Full per-file list with locations is in the Backlog table below (filter rows whe ## Backlog -*Auto-derived from `git grep -nE 'TODO\[scala3-port\]'` on this PR's tip. Total tags: 155.* +*Auto-derived from `git grep -nE 'TODO\[scala3-port\]'` on this PR's tip. Total tags: 154.* | Location | Description | Effort | |---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|--------| @@ -129,7 +135,6 @@ Full per-file list with locations is in the Backlog table below (filter rows whe | `core/src/main/scala/com/avsystem/commons/SharedExtensions.scala:145` | sourceCode (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/SharedExtensions.scala:147` | withSourceCode (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/annotation/AnnotationAggregate.scala:52` | reifyAggregated (Scala 2 macro def) | L | -| `core/src/main/scala/com/avsystem/commons/annotation/positioned.scala:12` | here (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/di/Components.scala:18` | component (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/di/Components.scala:21` | asyncComponent (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/di/Components.scala:25` | singleton (Scala 2 macro def) | L | @@ -173,7 +178,6 @@ Full per-file list with locations is in the Backlog table below (filter rows whe | `core/src/main/scala/com/avsystem/commons/misc/SelfInstance.scala:4` | C[_] existential narrowed to C[Any] (Scala 3 forbids HKT wildcard application) | S | | `core/src/main/scala/com/avsystem/commons/misc/SelfInstance.scala:7` | SelfInstance.materialize (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/misc/SimpleClassName.scala:8` | SimpleClassName.materialize (Scala 2 macro def) | L | -| `core/src/main/scala/com/avsystem/commons/misc/SourceInfo.scala:28` | SourceInfo.here (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/misc/Timestamp.scala:13` | Comparable[Timestamp] (Scala 3 forbids AnyVal inheriting Object-derived traits) | S | | `core/src/main/scala/com/avsystem/commons/misc/TypeString.scala:31` | TypeString.materialize (Scala 2 macro def) | L | | `core/src/main/scala/com/avsystem/commons/misc/TypeString.scala:90` | JavaClassName.materialize (Scala 2 macro def) | L | From a79fb8102025f199e05eeb49fe59808e74ad175d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Mon, 1 Jun 2026 15:58:55 +0200 Subject: [PATCH 4/5] build: re-enable sbt-ci-release plugin (worktree-only workaround leaked into branch) --- project/plugins.sbt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index f1e3a9300..06aaa06ef 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,8 +5,7 @@ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.21.0") addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.2") addSbtPlugin("org.jetbrains.scala" % "sbt-ide-settings" % "1.1.4") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8") -// TODO[scala3-port]: re-enable sbt-ci-release — transitively pulls sbt-git whose JGit chokes on linked worktrees (NoWorkTreeException). Disabled to unblock per-branch builds in worktrees. -//addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.6.1") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.1") From edbc3daf8602a085fcc6dbf69fcf9958aae39aeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Tue, 2 Jun 2026 15:12:48 +0200 Subject: [PATCH 5/5] test(core): expand PositionedTest with Matchers and additional offset assertion - use Matchers for improved assertion readability - add test to verify `positioned.here` offset corresponds to the term's start offset --- .../avsystem/commons/annotation/PositionedTest.scala | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/core/src/test/scala/com/avsystem/commons/annotation/PositionedTest.scala b/core/src/test/scala/com/avsystem/commons/annotation/PositionedTest.scala index 6b5ee3033..491cb3d9e 100644 --- a/core/src/test/scala/com/avsystem/commons/annotation/PositionedTest.scala +++ b/core/src/test/scala/com/avsystem/commons/annotation/PositionedTest.scala @@ -1,8 +1,12 @@ -package com.avsystem.commons.annotation +package com.avsystem.commons +package annotation import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class PositionedTest extends AnyFunSuite with Matchers { + val point: Int = positioned.here -class PositionedTest extends AnyFunSuite { test("positioned.here yields distinct positive offsets at distinct call sites") { val a = positioned.here val b = positioned.here @@ -10,4 +14,8 @@ class PositionedTest extends AnyFunSuite { assert(b > 0) assert(a != b) } + + test("positioned.here offset is the start offset of the `positioned.here` term") { + point shouldBe 214 + } }