From e53f8615737a056cbab567188a2a1007f300a9e7 Mon Sep 17 00:00:00 2001 From: Yichen Xu Date: Wed, 16 Jul 2025 18:46:42 +0800 Subject: [PATCH 1/9] Add setting for specifying magic line number comment [Cherry-picked b52e6b807ef5949765bff65eb1606a3bfeadb116] --- compiler/src/dotty/tools/dotc/config/ScalaSettings.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 5cdc742fa753..e8cc89e0dfef 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -341,6 +341,7 @@ private sealed trait XSettings: val Xdumpclasses: Setting[String] = StringSetting(AdvancedSetting, "Xdump-classes", "dir", "Dump the generated bytecode to .class files (useful for reflective compilation that utilizes in-memory classloaders).", "") val XjarCompressionLevel: Setting[Int] = IntChoiceSetting(AdvancedSetting, "Xjar-compression-level", "compression level to use when writing jar files", Deflater.DEFAULT_COMPRESSION to Deflater.BEST_COMPRESSION, Deflater.DEFAULT_COMPRESSION) val XkindProjector: Setting[String] = ChoiceSetting(AdvancedSetting, "Xkind-projector", "[underscores, enable, disable]", "Allow `*` as type lambda placeholder to be compatible with kind projector. When invoked as -Xkind-projector:underscores will repurpose `_` to be a type parameter placeholder, this will disable usage of underscore as a wildcard.", List("disable", "", "underscores"), "disable", legacyArgs = true) + val XmagicOffsetHeader: Setting[String] = StringSetting(AdvancedSetting, "Xmagic-offset-header", "header", "Specify the magic header comment that marks the start of the actual code in generated wrapper scripts.", "") /** Documentation related settings */ val XdropComments: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xdrop-docs", "Drop documentation when scanning source files.", aliases = List("-Xdrop-comments")) From cff7122d9645cd3cb9482cfc21b047cbc69af3a1 Mon Sep 17 00:00:00 2001 From: Yichen Xu Date: Thu, 17 Jul 2025 00:09:31 +0800 Subject: [PATCH 2/9] Offset line numbers when reporting given a magic comment [Cherry-picked f5f299615013696e124417f8edb07335333a661d] --- .../tools/dotc/reporting/MessageRendering.scala | 6 ++++-- .../src/dotty/tools/dotc/rewrites/Rewrites.scala | 2 +- .../src/dotty/tools/dotc/util/SourceFile.scala | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala index 7fddfc8d6ed0..8f9a00058ad5 100644 --- a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala +++ b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala @@ -11,7 +11,7 @@ import core.Decorators.* import printing.Highlighting.{Blue, Red, Yellow} import printing.SyntaxHighlighting import Diagnostic.* -import util.{ SourcePosition, NoSourcePosition } +import util.{ SourcePosition, NoSourcePosition, WrappedSourceFile } import util.Chars.{ LF, CR, FF, SU } import scala.annotation.switch @@ -44,7 +44,9 @@ trait MessageRendering { var maxLen = Int.MinValue def render(offsetAndLine: (Int, String)): String = { val (offset1, line) = offsetAndLine - val lineNbr = (pos.source.offsetToLine(offset1) + 1).toString + val magicOffset = WrappedSourceFile.locateMagicHeader(pos.source).getOrElse(0) + println(i"magicOffset: $magicOffset") + val lineNbr = (pos.source.offsetToLine(offset1) + 1 - magicOffset).toString val prefix = String.format(s"%${offset - 2}s |", lineNbr) maxLen = math.max(maxLen, prefix.length) val lnum = hl(" " * math.max(0, maxLen - prefix.length - 1) + prefix) diff --git a/compiler/src/dotty/tools/dotc/rewrites/Rewrites.scala b/compiler/src/dotty/tools/dotc/rewrites/Rewrites.scala index 3c7216625a7c..272db26bdd3c 100644 --- a/compiler/src/dotty/tools/dotc/rewrites/Rewrites.scala +++ b/compiler/src/dotty/tools/dotc/rewrites/Rewrites.scala @@ -7,7 +7,7 @@ import core.Contexts.* import collection.mutable import scala.annotation.tailrec import dotty.tools.dotc.reporting.Reporter -import dotty.tools.dotc.util.SourcePosition; +import dotty.tools.dotc.util.SourcePosition import java.io.OutputStreamWriter import java.nio.charset.StandardCharsets.UTF_8 diff --git a/compiler/src/dotty/tools/dotc/util/SourceFile.scala b/compiler/src/dotty/tools/dotc/util/SourceFile.scala index eb99fe99d926..971b614c085a 100644 --- a/compiler/src/dotty/tools/dotc/util/SourceFile.scala +++ b/compiler/src/dotty/tools/dotc/util/SourceFile.scala @@ -61,6 +61,21 @@ object ScriptSourceFile { } } +object WrappedSourceFile: + private val cache: mutable.HashMap[SourceFile, Int] = mutable.HashMap.empty + def locateMagicHeader(sourceFile: SourceFile)(using Context): Option[Int] = + def findOffset: Int = + val magicHeader = ctx.settings.XmagicOffsetHeader.value + if magicHeader.isEmpty then + -1 + else + val s = new String(sourceFile.content) + val regex = ("(?m)^" + java.util.regex.Pattern.quote(magicHeader) + "$").r + val pos = regex.findFirstMatchIn(s).map(_.start).map(sourceFile.offsetToLine(_)) + pos.getOrElse(-1) + val result = cache.getOrElseUpdate(sourceFile, findOffset) + if result >= 0 then Some(result + 1) else None + class SourceFile(val file: AbstractFile, computeContent: => Array[Char]) extends interfaces.SourceFile { import SourceFile.* From fc0593cb2b6b042e9d9c9e779fbd0c9f4ef63850 Mon Sep 17 00:00:00 2001 From: Yichen Xu Date: Thu, 17 Jul 2025 00:39:49 +0800 Subject: [PATCH 3/9] Add testcases [Cherry-picked f89d9cc998f5204d1114495a2268dd54c29c8206] --- .../dotty/tools/dotc/config/ScalaSettings.scala | 2 +- .../tools/dotc/reporting/MessageRendering.scala | 7 ++++--- tests/neg/magic-offset-header-a.check | 7 +++++++ tests/neg/magic-offset-header-a.scala | 6 ++++++ tests/neg/magic-offset-header-b.check | 14 ++++++++++++++ tests/neg/magic-offset-header-b.scala | 7 +++++++ tests/neg/magic-offset-header-c.check | 7 +++++++ tests/neg/magic-offset-header-c.scala | 8 ++++++++ 8 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 tests/neg/magic-offset-header-a.check create mode 100644 tests/neg/magic-offset-header-a.scala create mode 100644 tests/neg/magic-offset-header-b.check create mode 100644 tests/neg/magic-offset-header-b.scala create mode 100644 tests/neg/magic-offset-header-c.check create mode 100644 tests/neg/magic-offset-header-c.scala diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index e8cc89e0dfef..1304f31ed9cd 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -341,7 +341,7 @@ private sealed trait XSettings: val Xdumpclasses: Setting[String] = StringSetting(AdvancedSetting, "Xdump-classes", "dir", "Dump the generated bytecode to .class files (useful for reflective compilation that utilizes in-memory classloaders).", "") val XjarCompressionLevel: Setting[Int] = IntChoiceSetting(AdvancedSetting, "Xjar-compression-level", "compression level to use when writing jar files", Deflater.DEFAULT_COMPRESSION to Deflater.BEST_COMPRESSION, Deflater.DEFAULT_COMPRESSION) val XkindProjector: Setting[String] = ChoiceSetting(AdvancedSetting, "Xkind-projector", "[underscores, enable, disable]", "Allow `*` as type lambda placeholder to be compatible with kind projector. When invoked as -Xkind-projector:underscores will repurpose `_` to be a type parameter placeholder, this will disable usage of underscore as a wildcard.", List("disable", "", "underscores"), "disable", legacyArgs = true) - val XmagicOffsetHeader: Setting[String] = StringSetting(AdvancedSetting, "Xmagic-offset-header", "header", "Specify the magic header comment that marks the start of the actual code in generated wrapper scripts.", "") + val XmagicOffsetHeader: Setting[String] = StringSetting(AdvancedSetting, "Xmagic-offset-header", "header", "Specify the magic header comment that marks the start of the actual code in generated wrapper scripts. Example: -Xmagic-offset-header:///SOURCE_CODE_START", "") /** Documentation related settings */ val XdropComments: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xdrop-docs", "Drop documentation when scanning source files.", aliases = List("-Xdrop-comments")) diff --git a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala index 8f9a00058ad5..dcb9ddcb2d06 100644 --- a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala +++ b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala @@ -44,9 +44,10 @@ trait MessageRendering { var maxLen = Int.MinValue def render(offsetAndLine: (Int, String)): String = { val (offset1, line) = offsetAndLine - val magicOffset = WrappedSourceFile.locateMagicHeader(pos.source).getOrElse(0) - println(i"magicOffset: $magicOffset") - val lineNbr = (pos.source.offsetToLine(offset1) + 1 - magicOffset).toString + var magicOffset = WrappedSourceFile.locateMagicHeader(pos.source).getOrElse(0) + val lineId = pos.source.offsetToLine(offset1) + if lineId < magicOffset then magicOffset = 0 + val lineNbr = (lineId + 1 - magicOffset).toString val prefix = String.format(s"%${offset - 2}s |", lineNbr) maxLen = math.max(maxLen, prefix.length) val lnum = hl(" " * math.max(0, maxLen - prefix.length - 1) + prefix) diff --git a/tests/neg/magic-offset-header-a.check b/tests/neg/magic-offset-header-a.check new file mode 100644 index 000000000000..c4db6f3c5a12 --- /dev/null +++ b/tests/neg/magic-offset-header-a.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg/magic-offset-header-a.scala:6:19 ---------------------------------------------- +1 |def test1(): Int = "无穷" // error + | ^^^^ + | Found: ("无穷" : String) + | Required: Int + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/magic-offset-header-a.scala b/tests/neg/magic-offset-header-a.scala new file mode 100644 index 000000000000..f125cbb56b70 --- /dev/null +++ b/tests/neg/magic-offset-header-a.scala @@ -0,0 +1,6 @@ +//> using options -Xmagic-offset-header:///TEST_MARKER +val t1 = 1 +val t2 = 2 +val t3 = 3 +///TEST_MARKER +def test1(): Int = "无穷" // error diff --git a/tests/neg/magic-offset-header-b.check b/tests/neg/magic-offset-header-b.check new file mode 100644 index 000000000000..aca2ee070145 --- /dev/null +++ b/tests/neg/magic-offset-header-b.check @@ -0,0 +1,14 @@ +-- [E007] Type Mismatch Error: tests/neg/magic-offset-header-b.scala:3:13 ---------------------------------------------- +3 |def x: Int = true // error + | ^^^^ + | Found: (true : Boolean) + | Required: Int + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/magic-offset-header-b.scala:7:13 ---------------------------------------------- +2 |def y: Int = false // error + | ^^^^^ + | Found: (false : Boolean) + | Required: Int + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/magic-offset-header-b.scala b/tests/neg/magic-offset-header-b.scala new file mode 100644 index 000000000000..a498335ecd31 --- /dev/null +++ b/tests/neg/magic-offset-header-b.scala @@ -0,0 +1,7 @@ +//> using options -Xmagic-offset-header:///TEST_MARKER + +def x: Int = true // error + +///TEST_MARKER + +def y: Int = false // error diff --git a/tests/neg/magic-offset-header-c.check b/tests/neg/magic-offset-header-c.check new file mode 100644 index 000000000000..504b65b7f95b --- /dev/null +++ b/tests/neg/magic-offset-header-c.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg/magic-offset-header-c.scala:8:18 ---------------------------------------------- +3 | val x: String = 0 // error + | ^ + | Found: (0 : Int) + | Required: String + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/magic-offset-header-c.scala b/tests/neg/magic-offset-header-c.scala new file mode 100644 index 000000000000..0e1a1bfca9e3 --- /dev/null +++ b/tests/neg/magic-offset-header-c.scala @@ -0,0 +1,8 @@ +//> using options -Xmagic-offset-header:///SOURCE_CODE_START_MARKER + +val generatedCode = 123 + +///SOURCE_CODE_START_MARKER + +def userCode = + val x: String = 0 // error From eaebc99ca12f71bd763181e892ccd9c2d98a67cb Mon Sep 17 00:00:00 2001 From: Yichen Xu Date: Fri, 25 Jul 2025 15:50:06 +0800 Subject: [PATCH 4/9] Move magic header setting to -Y and add path setting [Cherry-picked d0ed7118baeecd0c2861ab302ccad592ac93f6e6] --- compiler/src/dotty/tools/dotc/config/ScalaSettings.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 1304f31ed9cd..bad59637254b 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -341,7 +341,6 @@ private sealed trait XSettings: val Xdumpclasses: Setting[String] = StringSetting(AdvancedSetting, "Xdump-classes", "dir", "Dump the generated bytecode to .class files (useful for reflective compilation that utilizes in-memory classloaders).", "") val XjarCompressionLevel: Setting[Int] = IntChoiceSetting(AdvancedSetting, "Xjar-compression-level", "compression level to use when writing jar files", Deflater.DEFAULT_COMPRESSION to Deflater.BEST_COMPRESSION, Deflater.DEFAULT_COMPRESSION) val XkindProjector: Setting[String] = ChoiceSetting(AdvancedSetting, "Xkind-projector", "[underscores, enable, disable]", "Allow `*` as type lambda placeholder to be compatible with kind projector. When invoked as -Xkind-projector:underscores will repurpose `_` to be a type parameter placeholder, this will disable usage of underscore as a wildcard.", List("disable", "", "underscores"), "disable", legacyArgs = true) - val XmagicOffsetHeader: Setting[String] = StringSetting(AdvancedSetting, "Xmagic-offset-header", "header", "Specify the magic header comment that marks the start of the actual code in generated wrapper scripts. Example: -Xmagic-offset-header:///SOURCE_CODE_START", "") /** Documentation related settings */ val XdropComments: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xdrop-docs", "Drop documentation when scanning source files.", aliases = List("-Xdrop-comments")) @@ -445,6 +444,9 @@ private sealed trait YSettings: val YbestEffort: Setting[Boolean] = BooleanSetting(ForkSetting, "Ybest-effort", "Enable best-effort compilation attempting to produce betasty to the META-INF/best-effort directory, regardless of errors, as part of the pickler phase.") val YwithBestEffortTasty: Setting[Boolean] = BooleanSetting(ForkSetting, "Ywith-best-effort-tasty", "Allow to compile using best-effort tasty files. If such file is used, the compiler will stop after the pickler phase.") + val YmagicOffsetHeader: Setting[String] = StringSetting(AdvancedSetting, "Ymagic-offset-header", "header", "Specify the magic header comment that marks the start of the actual code in generated wrapper scripts. Example: -Ymagic-offset-header:SOURCE_CODE_START", "") + val YoriginalSourceHeader: Setting[String] = StringSetting(AdvancedSetting, "Yoriginal-source-header", "header", "Specify the magic header comment that marks the path of the original code in generated wrapper scripts. Example: -Ymagic-offset-header:ORIGINAL_SOURCE_CODE_PATH", "") + // Experimental language features @deprecated(message = "This flag has no effect and will be removed in a future version.", since = "3.7.0") val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-kind-polymorphism", "Disable kind polymorphism. (This flag has no effect)", deprecation = Deprecation.removed()) From c945ae8001398d8fbf95b1f7a1980d4679aebf12 Mon Sep 17 00:00:00 2001 From: Yichen Xu Date: Thu, 31 Jul 2025 16:00:01 +0800 Subject: [PATCH 5/9] Cleanup [Cherry-picked 36aac1e660ac4d96705683c7384b6bd1baa55f87] --- compiler/src/dotty/tools/dotc/config/ScalaSettings.scala | 3 +-- .../src/dotty/tools/dotc/reporting/MessageRendering.scala | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index bad59637254b..7e806fbd65f2 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -444,8 +444,7 @@ private sealed trait YSettings: val YbestEffort: Setting[Boolean] = BooleanSetting(ForkSetting, "Ybest-effort", "Enable best-effort compilation attempting to produce betasty to the META-INF/best-effort directory, regardless of errors, as part of the pickler phase.") val YwithBestEffortTasty: Setting[Boolean] = BooleanSetting(ForkSetting, "Ywith-best-effort-tasty", "Allow to compile using best-effort tasty files. If such file is used, the compiler will stop after the pickler phase.") - val YmagicOffsetHeader: Setting[String] = StringSetting(AdvancedSetting, "Ymagic-offset-header", "header", "Specify the magic header comment that marks the start of the actual code in generated wrapper scripts. Example: -Ymagic-offset-header:SOURCE_CODE_START", "") - val YoriginalSourceHeader: Setting[String] = StringSetting(AdvancedSetting, "Yoriginal-source-header", "header", "Specify the magic header comment that marks the path of the original code in generated wrapper scripts. Example: -Ymagic-offset-header:ORIGINAL_SOURCE_CODE_PATH", "") + val YmagicOffsetHeader: Setting[String] = StringSetting(AdvancedSetting, "Ymagic-offset-header", "header", "Specify the magic header comment that marks the start of the actual code in generated wrapper scripts. Example: -Ymagic-offset-header:SOURCE_CODE_START. Then, in the source, the magic comment `///SOURCE_CODE_START` marks the start of user code. The comment can be optionally suffixed by `:` to indicate the original file.", "") // Experimental language features @deprecated(message = "This flag has no effect and will be removed in a future version.", since = "3.7.0") diff --git a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala index dcb9ddcb2d06..0414bdb3c58b 100644 --- a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala +++ b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala @@ -11,7 +11,7 @@ import core.Decorators.* import printing.Highlighting.{Blue, Red, Yellow} import printing.SyntaxHighlighting import Diagnostic.* -import util.{ SourcePosition, NoSourcePosition, WrappedSourceFile } +import util.{SourcePosition, NoSourcePosition} import util.Chars.{ LF, CR, FF, SU } import scala.annotation.switch @@ -44,10 +44,7 @@ trait MessageRendering { var maxLen = Int.MinValue def render(offsetAndLine: (Int, String)): String = { val (offset1, line) = offsetAndLine - var magicOffset = WrappedSourceFile.locateMagicHeader(pos.source).getOrElse(0) - val lineId = pos.source.offsetToLine(offset1) - if lineId < magicOffset then magicOffset = 0 - val lineNbr = (lineId + 1 - magicOffset).toString + val lineNbr = (pos.source.offsetToLine(offset1) + 1).toString val prefix = String.format(s"%${offset - 2}s |", lineNbr) maxLen = math.max(maxLen, prefix.length) val lnum = hl(" " * math.max(0, maxLen - prefix.length - 1) + prefix) From 21d347a1c765115c4f2072c9fee9983429d444a0 Mon Sep 17 00:00:00 2001 From: Yichen Xu Date: Thu, 31 Jul 2025 17:27:47 +0800 Subject: [PATCH 6/9] Reposition span to original file for wrapper scripts [Cherry-picked 48c432238b47d85f14b932c1f4eb85a6fc5cefed] --- .../src/dotty/tools/dotc/ast/Positioned.scala | 10 ++++- .../tools/dotc/config/ScalaSettings.scala | 2 +- .../dotty/tools/dotc/util/SourceFile.scala | 38 +++++++++++++------ 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Positioned.scala b/compiler/src/dotty/tools/dotc/ast/Positioned.scala index d8017783f47f..0ab3e1ea6072 100644 --- a/compiler/src/dotty/tools/dotc/ast/Positioned.scala +++ b/compiler/src/dotty/tools/dotc/ast/Positioned.scala @@ -3,7 +3,8 @@ package dotc package ast import util.Spans.* -import util.{SourceFile, SourcePosition, SrcPos} +import util.{SourceFile, SourcePosition, SrcPos, WrappedSourceFile} +import WrappedSourceFile.MagicHeaderInfo, MagicHeaderInfo.* import core.Contexts.* import core.Decorators.* import core.NameOps.* @@ -51,7 +52,12 @@ abstract class Positioned(implicit @constructorOnly src: SourceFile) extends Src def source: SourceFile = mySource - def sourcePos(using Context): SourcePosition = source.atSpan(span) + def sourcePos(using Context): SourcePosition = + val info = WrappedSourceFile.locateMagicHeader(source) + info match + case HasHeader(offset, originalFile) => + originalFile.atSpan(span `shift` -offset) + case _ => source.atSpan(span) /** This positioned item, widened to `SrcPos`. Used to make clear we only need the * position, typically for error reporting. diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 7e806fbd65f2..a3fa5fd9bb06 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -444,7 +444,7 @@ private sealed trait YSettings: val YbestEffort: Setting[Boolean] = BooleanSetting(ForkSetting, "Ybest-effort", "Enable best-effort compilation attempting to produce betasty to the META-INF/best-effort directory, regardless of errors, as part of the pickler phase.") val YwithBestEffortTasty: Setting[Boolean] = BooleanSetting(ForkSetting, "Ywith-best-effort-tasty", "Allow to compile using best-effort tasty files. If such file is used, the compiler will stop after the pickler phase.") - val YmagicOffsetHeader: Setting[String] = StringSetting(AdvancedSetting, "Ymagic-offset-header", "header", "Specify the magic header comment that marks the start of the actual code in generated wrapper scripts. Example: -Ymagic-offset-header:SOURCE_CODE_START. Then, in the source, the magic comment `///SOURCE_CODE_START` marks the start of user code. The comment can be optionally suffixed by `:` to indicate the original file.", "") + val YmagicOffsetHeader: Setting[String] = StringSetting(ForkSetting, "Ymagic-offset-header", "header", "Specify the magic header comment that marks the start of the actual code in generated wrapper scripts. Example: -Ymagic-offset-header:SOURCE_CODE_START. Then, in the source, the magic comment `///SOURCE_CODE_START:` marks the start of user code. The comment should be suffixed by `:` to indicate the original file.", "") // Experimental language features @deprecated(message = "This flag has no effect and will be removed in a future version.", since = "3.7.0") diff --git a/compiler/src/dotty/tools/dotc/util/SourceFile.scala b/compiler/src/dotty/tools/dotc/util/SourceFile.scala index 971b614c085a..9cc3dc5e731d 100644 --- a/compiler/src/dotty/tools/dotc/util/SourceFile.scala +++ b/compiler/src/dotty/tools/dotc/util/SourceFile.scala @@ -7,6 +7,7 @@ import scala.language.unsafeNulls import dotty.tools.io.* import Spans.* import core.Contexts.* +import core.Decorators.* import scala.io.Codec import Chars.* @@ -62,19 +63,34 @@ object ScriptSourceFile { } object WrappedSourceFile: - private val cache: mutable.HashMap[SourceFile, Int] = mutable.HashMap.empty - def locateMagicHeader(sourceFile: SourceFile)(using Context): Option[Int] = - def findOffset: Int = - val magicHeader = ctx.settings.XmagicOffsetHeader.value - if magicHeader.isEmpty then - -1 + enum MagicHeaderInfo: + case HasHeader(offset: Int, originalFile: SourceFile) + case NoHeader + import MagicHeaderInfo.* + + private val cache: mutable.HashMap[SourceFile, MagicHeaderInfo] = mutable.HashMap.empty + + def locateMagicHeader(sourceFile: SourceFile)(using Context): MagicHeaderInfo = + def findOffset: MagicHeaderInfo = + val magicHeader = ctx.settings.YmagicOffsetHeader.value + if magicHeader.isEmpty then NoHeader else - val s = new String(sourceFile.content) - val regex = ("(?m)^" + java.util.regex.Pattern.quote(magicHeader) + "$").r - val pos = regex.findFirstMatchIn(s).map(_.start).map(sourceFile.offsetToLine(_)) - pos.getOrElse(-1) + val text = new String(sourceFile.content) + val headerQuoted = java.util.regex.Pattern.quote("///" + magicHeader) + val regex = s"(?m)^$headerQuoted:(.+)$$".r + regex.findFirstMatchIn(text) match + case Some(m) => + val markerOffset = m.start + val sourceStartOffset = sourceFile.nextLine(markerOffset) + val file = ctx.getFile(m.group(1)) + if file.exists then + HasHeader(sourceStartOffset, ctx.getSource(file)) + else + report.warning(em"original source file not found: ${file.path}") + NoHeader + case None => NoHeader val result = cache.getOrElseUpdate(sourceFile, findOffset) - if result >= 0 then Some(result + 1) else None + result class SourceFile(val file: AbstractFile, computeContent: => Array[Char]) extends interfaces.SourceFile { import SourceFile.* From 1258fc166b0cd8f9a7781b7bebe439411755fd57 Mon Sep 17 00:00:00 2001 From: Yichen Xu Date: Thu, 31 Jul 2025 23:37:42 +0800 Subject: [PATCH 7/9] Adapt test cases [Cherry-picked 7eaf07acc07ae212472da1e3cc414435334b44b5] --- tests/neg/magic-offset-header-a.scala | 6 +----- ...header-a.check => magic-offset-header-a_wrapper.check} | 4 ++-- tests/neg/magic-offset-header-a_wrapper.scala | 7 +++++++ tests/neg/magic-offset-header-b.scala | 5 ----- ...header-b.check => magic-offset-header-b_wrapper.check} | 4 ++-- tests/neg/magic-offset-header-b_wrapper.scala | 7 +++++++ tests/neg/magic-offset-header-c.check | 7 ------- tests/neg/magic-offset-header-c.scala | 5 ----- tests/neg/magic-offset-header-c_wrapper.check | 8 ++++++++ tests/neg/magic-offset-header-c_wrapper.scala | 8 ++++++++ 10 files changed, 35 insertions(+), 26 deletions(-) rename tests/neg/{magic-offset-header-a.check => magic-offset-header-a_wrapper.check} (71%) create mode 100644 tests/neg/magic-offset-header-a_wrapper.scala rename tests/neg/{magic-offset-header-b.check => magic-offset-header-b_wrapper.check} (71%) create mode 100644 tests/neg/magic-offset-header-b_wrapper.scala delete mode 100644 tests/neg/magic-offset-header-c.check create mode 100644 tests/neg/magic-offset-header-c_wrapper.check create mode 100644 tests/neg/magic-offset-header-c_wrapper.scala diff --git a/tests/neg/magic-offset-header-a.scala b/tests/neg/magic-offset-header-a.scala index f125cbb56b70..48267a7853f0 100644 --- a/tests/neg/magic-offset-header-a.scala +++ b/tests/neg/magic-offset-header-a.scala @@ -1,6 +1,2 @@ -//> using options -Xmagic-offset-header:///TEST_MARKER -val t1 = 1 -val t2 = 2 -val t3 = 3 -///TEST_MARKER + def test1(): Int = "无穷" // error diff --git a/tests/neg/magic-offset-header-a.check b/tests/neg/magic-offset-header-a_wrapper.check similarity index 71% rename from tests/neg/magic-offset-header-a.check rename to tests/neg/magic-offset-header-a_wrapper.check index c4db6f3c5a12..0ab253c12804 100644 --- a/tests/neg/magic-offset-header-a.check +++ b/tests/neg/magic-offset-header-a_wrapper.check @@ -1,5 +1,5 @@ --- [E007] Type Mismatch Error: tests/neg/magic-offset-header-a.scala:6:19 ---------------------------------------------- -1 |def test1(): Int = "无穷" // error +-- [E007] Type Mismatch Error: tests/neg/magic-offset-header-a.scala:2:19 ---------------------------------------------- +2 |def test1(): Int = "无穷" // error | ^^^^ | Found: ("无穷" : String) | Required: Int diff --git a/tests/neg/magic-offset-header-a_wrapper.scala b/tests/neg/magic-offset-header-a_wrapper.scala new file mode 100644 index 000000000000..af4f2b8bf8dd --- /dev/null +++ b/tests/neg/magic-offset-header-a_wrapper.scala @@ -0,0 +1,7 @@ +//> using options -Ymagic-offset-header:TEST_MARKER +val t1 = 1 +val t2 = 2 +val t3 = 3 +///TEST_MARKER:tests/neg/magic-offset-header-a.scala + +def test1(): Int = "无穷" // anypos-error diff --git a/tests/neg/magic-offset-header-b.scala b/tests/neg/magic-offset-header-b.scala index a498335ecd31..aeb569272523 100644 --- a/tests/neg/magic-offset-header-b.scala +++ b/tests/neg/magic-offset-header-b.scala @@ -1,7 +1,2 @@ -//> using options -Xmagic-offset-header:///TEST_MARKER - -def x: Int = true // error - -///TEST_MARKER def y: Int = false // error diff --git a/tests/neg/magic-offset-header-b.check b/tests/neg/magic-offset-header-b_wrapper.check similarity index 71% rename from tests/neg/magic-offset-header-b.check rename to tests/neg/magic-offset-header-b_wrapper.check index aca2ee070145..5d862e5a6b10 100644 --- a/tests/neg/magic-offset-header-b.check +++ b/tests/neg/magic-offset-header-b_wrapper.check @@ -1,11 +1,11 @@ --- [E007] Type Mismatch Error: tests/neg/magic-offset-header-b.scala:3:13 ---------------------------------------------- +-- [E007] Type Mismatch Error: tests/neg/magic-offset-header-b_wrapper.scala:3:13 -------------------------------------- 3 |def x: Int = true // error | ^^^^ | Found: (true : Boolean) | Required: Int | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg/magic-offset-header-b.scala:7:13 ---------------------------------------------- +-- [E007] Type Mismatch Error: tests/neg/magic-offset-header-b.scala:2:13 ---------------------------------------------- 2 |def y: Int = false // error | ^^^^^ | Found: (false : Boolean) diff --git a/tests/neg/magic-offset-header-b_wrapper.scala b/tests/neg/magic-offset-header-b_wrapper.scala new file mode 100644 index 000000000000..ef0e552948d3 --- /dev/null +++ b/tests/neg/magic-offset-header-b_wrapper.scala @@ -0,0 +1,7 @@ +//> using options -Ymagic-offset-header:TEST_MARKER + +def x: Int = true // error + +///TEST_MARKER:tests/neg/magic-offset-header-b.scala + +def y: Int = false // anypos-error diff --git a/tests/neg/magic-offset-header-c.check b/tests/neg/magic-offset-header-c.check deleted file mode 100644 index 504b65b7f95b..000000000000 --- a/tests/neg/magic-offset-header-c.check +++ /dev/null @@ -1,7 +0,0 @@ --- [E007] Type Mismatch Error: tests/neg/magic-offset-header-c.scala:8:18 ---------------------------------------------- -3 | val x: String = 0 // error - | ^ - | Found: (0 : Int) - | Required: String - | - | longer explanation available when compiling with `-explain` diff --git a/tests/neg/magic-offset-header-c.scala b/tests/neg/magic-offset-header-c.scala index 0e1a1bfca9e3..be3cb333abff 100644 --- a/tests/neg/magic-offset-header-c.scala +++ b/tests/neg/magic-offset-header-c.scala @@ -1,8 +1,3 @@ -//> using options -Xmagic-offset-header:///SOURCE_CODE_START_MARKER - -val generatedCode = 123 - -///SOURCE_CODE_START_MARKER def userCode = val x: String = 0 // error diff --git a/tests/neg/magic-offset-header-c_wrapper.check b/tests/neg/magic-offset-header-c_wrapper.check new file mode 100644 index 000000000000..ee2eb879cf34 --- /dev/null +++ b/tests/neg/magic-offset-header-c_wrapper.check @@ -0,0 +1,8 @@ +bad option '-Xmagic-offset-header:SOURCE_CODE_START_MARKER' was ignored +-- [E007] Type Mismatch Error: tests/neg/magic-offset-header-c_wrapper.scala:8:18 -------------------------------------- +8 | val x: String = 0 // anypos-error + | ^ + | Found: (0 : Int) + | Required: String + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/magic-offset-header-c_wrapper.scala b/tests/neg/magic-offset-header-c_wrapper.scala new file mode 100644 index 000000000000..99ddd5b86df2 --- /dev/null +++ b/tests/neg/magic-offset-header-c_wrapper.scala @@ -0,0 +1,8 @@ +//> using options -Xmagic-offset-header:SOURCE_CODE_START_MARKER + +val generatedCode = 123 + +///SOURCE_CODE_START_MARKER:tests/neg/magic-offset-header-c.scala + +def userCode = + val x: String = 0 // anypos-error From 31bf8c32f5b4e2e5d912b284d08be2a8feb34aaf Mon Sep 17 00:00:00 2001 From: Yichen Xu Date: Thu, 31 Jul 2025 23:39:52 +0800 Subject: [PATCH 8/9] Only shift spans in user code [Cherry-picked 438b4c159758206484c150fdddc27dea7374e2ed] --- compiler/src/dotty/tools/dotc/ast/Positioned.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Positioned.scala b/compiler/src/dotty/tools/dotc/ast/Positioned.scala index 0ab3e1ea6072..5b57733eaeb1 100644 --- a/compiler/src/dotty/tools/dotc/ast/Positioned.scala +++ b/compiler/src/dotty/tools/dotc/ast/Positioned.scala @@ -56,7 +56,10 @@ abstract class Positioned(implicit @constructorOnly src: SourceFile) extends Src val info = WrappedSourceFile.locateMagicHeader(source) info match case HasHeader(offset, originalFile) => - originalFile.atSpan(span `shift` -offset) + if span.start >= offset then // This span is in user code + originalFile.atSpan(span.shift(-offset)) + else // Otherwise, return the source position in the wrapper code + source.atSpan(span) case _ => source.atSpan(span) /** This positioned item, widened to `SrcPos`. Used to make clear we only need the From 10f09410e16f4d3620c9fd894240efe753b9c28b Mon Sep 17 00:00:00 2001 From: Yichen Xu Date: Thu, 31 Jul 2025 23:44:08 +0800 Subject: [PATCH 9/9] Add test on missing original file [Cherry-picked cc8b04a4976f66a1dffb2a2ba9d0d62698d87a23] --- tests/neg/magic-offset-header-c_wrapper.check | 5 ++--- tests/neg/magic-offset-header-c_wrapper.scala | 2 +- tests/neg/magic-offset-header-d_wrapper.check | 15 +++++++++++++++ tests/neg/magic-offset-header-d_wrapper.scala | 5 +++++ 4 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 tests/neg/magic-offset-header-d_wrapper.check create mode 100644 tests/neg/magic-offset-header-d_wrapper.scala diff --git a/tests/neg/magic-offset-header-c_wrapper.check b/tests/neg/magic-offset-header-c_wrapper.check index ee2eb879cf34..0c33f5ea0338 100644 --- a/tests/neg/magic-offset-header-c_wrapper.check +++ b/tests/neg/magic-offset-header-c_wrapper.check @@ -1,6 +1,5 @@ -bad option '-Xmagic-offset-header:SOURCE_CODE_START_MARKER' was ignored --- [E007] Type Mismatch Error: tests/neg/magic-offset-header-c_wrapper.scala:8:18 -------------------------------------- -8 | val x: String = 0 // anypos-error +-- [E007] Type Mismatch Error: tests/neg/magic-offset-header-c.scala:3:18 ---------------------------------------------- +3 | val x: String = 0 // error | ^ | Found: (0 : Int) | Required: String diff --git a/tests/neg/magic-offset-header-c_wrapper.scala b/tests/neg/magic-offset-header-c_wrapper.scala index 99ddd5b86df2..51804a647fbe 100644 --- a/tests/neg/magic-offset-header-c_wrapper.scala +++ b/tests/neg/magic-offset-header-c_wrapper.scala @@ -1,4 +1,4 @@ -//> using options -Xmagic-offset-header:SOURCE_CODE_START_MARKER +//> using options -Ymagic-offset-header:SOURCE_CODE_START_MARKER val generatedCode = 123 diff --git a/tests/neg/magic-offset-header-d_wrapper.check b/tests/neg/magic-offset-header-d_wrapper.check new file mode 100644 index 000000000000..36421e857a1c --- /dev/null +++ b/tests/neg/magic-offset-header-d_wrapper.check @@ -0,0 +1,15 @@ +original source file not found: something_nonexist.scala +-- [E007] Type Mismatch Error: tests/neg/magic-offset-header-d_wrapper.scala:3:20 -------------------------------------- +3 |def test1: String = 0 // error + | ^ + | Found: (0 : Int) + | Required: String + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/magic-offset-header-d_wrapper.scala:5:17 -------------------------------------- +5 |def test2: Int = "0" // error + | ^^^ + | Found: ("0" : String) + | Required: Int + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/magic-offset-header-d_wrapper.scala b/tests/neg/magic-offset-header-d_wrapper.scala new file mode 100644 index 000000000000..85840e84b702 --- /dev/null +++ b/tests/neg/magic-offset-header-d_wrapper.scala @@ -0,0 +1,5 @@ +//> using options -Ymagic-offset-header:SOURCE_CODE_START_MARKER + +def test1: String = 0 // error +///SOURCE_CODE_START_MARKER:something_nonexist.scala +def test2: Int = "0" // error