Skip to content

Commit 8431d84

Browse files
authored
backend computes line number from source of position (#21763)
fixes #21762 This makes it possible to implement line number correction for Mill build files under Scala 3
2 parents f6bfa0a + 44ecf4b commit 8431d84

File tree

10 files changed

+138
-5
lines changed

10 files changed

+138
-5
lines changed

Diff for: compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala

+7-1
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,13 @@ trait BCodeSkelBuilder extends BCodeHelpers {
623623
}
624624

625625
if (emitLines && tree.span.exists && !tree.hasAttachment(SyntheticUnit)) {
626-
val nr = ctx.source.offsetToLine(tree.span.point) + 1
626+
val nr =
627+
val sourcePos = tree.sourcePos
628+
(
629+
if sourcePos.exists then sourcePos.source.positionInUltimateSource(sourcePos).line
630+
else ctx.source.offsetToLine(tree.span.point) // fallback
631+
) + 1
632+
627633
if (nr != lastEmittedLineNr) {
628634
lastEmittedLineNr = nr
629635
getNonLabelNode(lastInsn) match {

Diff for: compiler/src/dotty/tools/dotc/util/SourceFile.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ class SourceFile(val file: AbstractFile, computeContent: => Array[Char]) extends
119119
* For regular source files, simply return the argument.
120120
*/
121121
def positionInUltimateSource(position: SourcePosition): SourcePosition =
122-
SourcePosition(underlying, position.span shift start)
122+
if isSelfContained then position // return the argument
123+
else SourcePosition(underlying, position.span shift start)
123124

124125
private def calculateLineIndicesFromContents() = {
125126
val cs = content()

Diff for: compiler/src/dotty/tools/dotc/util/SourcePosition.scala

-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ extends SrcPos, interfaces.SourcePosition, Showable {
7979
rec(this)
8080
}
8181

82-
8382
override def toString: String =
8483
s"${if (source.exists) source.file.toString else "(no source)"}:$span"
8584

Diff for: compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala

+6-2
Original file line numberDiff line numberDiff line change
@@ -193,15 +193,18 @@ class BootstrappedOnlyCompilationTests {
193193

194194
// 1. hack with absolute path for -Xplugin
195195
// 2. copy `pluginFile` to destination
196-
def compileFilesInDir(dir: String): CompilationTest = {
196+
def compileFilesInDir(dir: String, run: Boolean = false): CompilationTest = {
197197
val outDir = defaultOutputDir + "testPlugins/"
198198
val sourceDir = new java.io.File(dir)
199199

200200
val dirs = sourceDir.listFiles.toList.filter(_.isDirectory)
201201
val targets = dirs.map { dir =>
202202
val compileDir = createOutputDirsForDir(dir, sourceDir, outDir)
203203
Files.copy(dir.toPath.resolve(pluginFile), compileDir.toPath.resolve(pluginFile), StandardCopyOption.REPLACE_EXISTING)
204-
val flags = TestFlags(withCompilerClasspath, noCheckOptions).and("-Xplugin:" + compileDir.getAbsolutePath)
204+
val flags = {
205+
val base = TestFlags(withCompilerClasspath, noCheckOptions).and("-Xplugin:" + compileDir.getAbsolutePath)
206+
if run then base.withRunClasspath(withCompilerClasspath) else base
207+
}
205208
SeparateCompilationSource("testPlugins", dir, flags, compileDir)
206209
}
207210

@@ -210,6 +213,7 @@ class BootstrappedOnlyCompilationTests {
210213

211214
compileFilesInDir("tests/plugins/neg").checkExpectedErrors()
212215
compileDir("tests/plugins/custom/analyzer", withCompilerOptions.and("-Yretain-trees")).checkCompile()
216+
compileFilesInDir("tests/plugins/run", run = true).checkRuns()
213217
}
214218
}
215219

Diff for: tests/plugins/run/scriptWrapper/Framework_1.scala

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package framework
2+
3+
class entrypoint extends scala.annotation.Annotation
+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package scriptWrapper
2+
3+
import dotty.tools.dotc.*
4+
import core.*
5+
import Contexts.Context
6+
import Contexts.ctx
7+
import plugins.*
8+
import ast.tpd
9+
import util.SourceFile
10+
11+
class LineNumberPlugin extends StandardPlugin {
12+
val name: String = "linenumbers"
13+
val description: String = "adjusts line numbers of script files"
14+
15+
override def initialize(options: List[String])(using Context): List[PluginPhase] = FixLineNumbers() :: Nil
16+
}
17+
18+
// Loosely follows Mill linenumbers plugin (scan for marker with "original" source, adjust line numbers to match)
19+
class FixLineNumbers extends PluginPhase {
20+
21+
val codeMarker = "//USER_CODE_HERE"
22+
23+
def phaseName: String = "fixLineNumbers"
24+
override def runsAfter: Set[String] = Set("posttyper")
25+
override def runsBefore: Set[String] = Set("pickler")
26+
27+
override def transformUnit(tree: tpd.Tree)(using Context): tpd.Tree = {
28+
val sourceContent = ctx.source.content()
29+
val lines = new String(sourceContent).linesWithSeparators.toVector
30+
val codeMarkerLine = lines.indexWhere(_.startsWith(codeMarker))
31+
32+
if codeMarkerLine < 0 then
33+
tree
34+
else
35+
val adjustedFile = lines.collectFirst {
36+
case s"//USER_SRC_FILE:./$file" => file.trim
37+
}.getOrElse("<unknown>")
38+
39+
val adjustedSrc = ctx.source.file.container.lookupName(adjustedFile, directory = false) match
40+
case null =>
41+
report.error(s"could not find file $adjustedFile", tree.sourcePos)
42+
return tree
43+
case file =>
44+
SourceFile(file, scala.io.Codec.UTF8)
45+
46+
val userCodeOffset = ctx.source.lineToOffset(codeMarkerLine + 1) // lines.take(codeMarkerLine).map(_.length).sum
47+
val lineMapper = LineMapper(codeMarkerLine, userCodeOffset, adjustedSrc)
48+
lineMapper.transform(tree)
49+
}
50+
51+
}
52+
53+
class LineMapper(markerLine: Int, userCodeOffset: Int, adjustedSrc: SourceFile) extends tpd.TreeMapWithPreciseStatContexts() {
54+
55+
override def transform(tree: tpd.Tree)(using Context): tpd.Tree = {
56+
val tree0 = super.transform(tree)
57+
val pos = tree0.sourcePos
58+
if pos.exists && pos.start >= userCodeOffset then
59+
val tree1 = tree0.cloneIn(adjustedSrc).withSpan(pos.span.shift(-userCodeOffset))
60+
// if tree1.show.toString == "???" then
61+
// val pos1 = tree1.sourcePos
62+
// sys.error(s"rewrote ??? at ${pos1.source}:${pos1.line + 1}:${pos1.column + 1} (sourced from ${markerLine + 2})")
63+
tree1
64+
else
65+
tree0
66+
}
67+
68+
}

Diff for: tests/plugins/run/scriptWrapper/Test_3.scala

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@main def Test: Unit = {
2+
val mainCls = Class.forName("foo_sc")
3+
val mainMethod = mainCls.getMethod("main", classOf[Array[String]])
4+
val stackTrace: Array[String] = {
5+
try
6+
mainMethod.invoke(null, Array.empty[String])
7+
sys.error("Expected an exception")
8+
catch
9+
case e: java.lang.reflect.InvocationTargetException =>
10+
val cause = e.getCause
11+
if cause != null then
12+
cause.getStackTrace.map(_.toString)
13+
else
14+
throw e
15+
}
16+
17+
val expected = Set(
18+
"foo_sc$.getRandom(foo_2.scala:3)", // adjusted line number (11 -> 3)
19+
"foo_sc$.brokenRandom(foo_2.scala:5)", // adjusted line number (13 -> 5)
20+
"foo_sc$.run(foo_2.scala:8)", // adjusted line number (16 -> 8)
21+
)
22+
23+
val missing = expected -- stackTrace
24+
assert(missing.isEmpty, s"Missing: $missing")
25+
}

Diff for: tests/plugins/run/scriptWrapper/foo_2.scala

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// generated code
2+
// script: foo.sc
3+
object foo_sc {
4+
def main(args: Array[String]): Unit = {
5+
run // assume some macro generates this by scanning for @entrypoint
6+
}
7+
//USER_SRC_FILE:./foo_original_2.scala
8+
//USER_CODE_HERE
9+
import framework.*
10+
11+
def getRandom: Int = brokenRandom // LINE 3;
12+
13+
def brokenRandom: Int = ??? // LINE 5;
14+
15+
@entrypoint
16+
def run = println("Hello, here is a random number: " + getRandom) // LINE 8;
17+
//END_USER_CODE_HERE
18+
}

Diff for: tests/plugins/run/scriptWrapper/foo_original_2.scala

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import framework.*
2+
3+
def getRandom: Int = brokenRandom // LINE 3;
4+
5+
def brokenRandom: Int = ??? // LINE 5;
6+
7+
@entrypoint
8+
def run = println("Hello, here is a random number: " + getRandom) // LINE 8;

Diff for: tests/plugins/run/scriptWrapper/plugin.properties

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pluginClass=scriptWrapper.LineNumberPlugin

0 commit comments

Comments
 (0)