Skip to content

Backport "Add line number magic comment support" to 3.7.3 #23646

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

Merged
merged 9 commits into from
Aug 1, 2025
Merged
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
13 changes: 11 additions & 2 deletions compiler/src/dotty/tools/dotc/ast/Positioned.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -51,7 +52,15 @@ 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) =>
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
* position, typically for error reporting.
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,8 @@ 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(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:<ORIGINAL_FILE_PATH>` marks the start of user code. The comment should be suffixed by `:<ORIGINAL_FILE_PATH>` 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")
val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting(ForkSetting, "Yno-kind-polymorphism", "Disable kind polymorphism. (This flag has no effect)", deprecation = Deprecation.removed())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
import util.Chars.{ LF, CR, FF, SU }
import scala.annotation.switch

Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/rewrites/Rewrites.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions compiler/src/dotty/tools/dotc/util/SourceFile.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -61,6 +62,36 @@ object ScriptSourceFile {
}
}

object WrappedSourceFile:
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 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)
result

class SourceFile(val file: AbstractFile, computeContent: => Array[Char]) extends interfaces.SourceFile {
import SourceFile.*

Expand Down
2 changes: 2 additions & 0 deletions tests/neg/magic-offset-header-a.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

def test1(): Int = "无穷" // error
7 changes: 7 additions & 0 deletions tests/neg/magic-offset-header-a_wrapper.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- [E007] Type Mismatch Error: tests/neg/magic-offset-header-a.scala:2:19 ----------------------------------------------
2 |def test1(): Int = "无穷" // error
| ^^^^
| Found: ("无穷" : String)
| Required: Int
|
| longer explanation available when compiling with `-explain`
7 changes: 7 additions & 0 deletions tests/neg/magic-offset-header-a_wrapper.scala
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions tests/neg/magic-offset-header-b.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

def y: Int = false // error
14 changes: 14 additions & 0 deletions tests/neg/magic-offset-header-b_wrapper.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- [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:2:13 ----------------------------------------------
2 |def y: Int = false // error
| ^^^^^
| Found: (false : Boolean)
| Required: Int
|
| longer explanation available when compiling with `-explain`
7 changes: 7 additions & 0 deletions tests/neg/magic-offset-header-b_wrapper.scala
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions tests/neg/magic-offset-header-c.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

def userCode =
val x: String = 0 // error
7 changes: 7 additions & 0 deletions tests/neg/magic-offset-header-c_wrapper.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- [E007] Type Mismatch Error: tests/neg/magic-offset-header-c.scala:3:18 ----------------------------------------------
3 | val x: String = 0 // error
| ^
| Found: (0 : Int)
| Required: String
|
| longer explanation available when compiling with `-explain`
8 changes: 8 additions & 0 deletions tests/neg/magic-offset-header-c_wrapper.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//> using options -Ymagic-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
15 changes: 15 additions & 0 deletions tests/neg/magic-offset-header-d_wrapper.check
Original file line number Diff line number Diff line change
@@ -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`
5 changes: 5 additions & 0 deletions tests/neg/magic-offset-header-d_wrapper.scala
Original file line number Diff line number Diff line change
@@ -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
Loading