Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
add demonstration of plugin adjusting positions
  • Loading branch information
bishabosha committed Oct 14, 2024
commit 54ddb427898fa1a245a4bed320c113b518ca6081
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,18 @@ class BootstrappedOnlyCompilationTests {

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

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

Expand All @@ -210,6 +213,7 @@ class BootstrappedOnlyCompilationTests {

compileFilesInDir("tests/plugins/neg").checkExpectedErrors()
compileDir("tests/plugins/custom/analyzer", withCompilerOptions.and("-Yretain-trees")).checkCompile()
compileFilesInDir("tests/plugins/run", run = true).checkRuns()
}
}

Expand Down
3 changes: 3 additions & 0 deletions tests/plugins/run/scriptWrapper/Framework_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package framework

class entrypoint extends scala.annotation.Annotation
68 changes: 68 additions & 0 deletions tests/plugins/run/scriptWrapper/LineNumberPlugin_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package scriptWrapper

import dotty.tools.dotc.*
import core.*
import Contexts.Context
import Contexts.ctx
import plugins.*
import ast.tpd
import util.SourceFile

class LineNumberPlugin extends StandardPlugin {
val name: String = "linenumbers"
val description: String = "adjusts line numbers of script files"

override def initialize(options: List[String])(using Context): List[PluginPhase] = FixLineNumbers() :: Nil
}

// Loosely follows Mill linenumbers plugin (scan for marker with "original" source, adjust line numbers to match)
class FixLineNumbers extends PluginPhase {

val codeMarker = "//USER_CODE_HERE"

def phaseName: String = "fixLineNumbers"
override def runsAfter: Set[String] = Set("posttyper")
override def runsBefore: Set[String] = Set("pickler")

override def transformUnit(tree: tpd.Tree)(using Context): tpd.Tree = {
val sourceContent = ctx.source.content()
val lines = new String(sourceContent).linesWithSeparators.toVector
val codeMarkerLine = lines.indexWhere(_.startsWith(codeMarker))

if codeMarkerLine < 0 then
tree
else
val adjustedFile = lines.collectFirst {
case s"//USER_SRC_FILE:./$file" => file.trim
}.getOrElse("<unknown>")

val adjustedSrc = ctx.source.file.container.lookupName(adjustedFile, directory = false) match
case null =>
report.error(s"could not find file $adjustedFile", tree.sourcePos)
return tree
case file =>
SourceFile(file, scala.io.Codec.UTF8)

val userCodeOffset = ctx.source.lineToOffset(codeMarkerLine + 1) // lines.take(codeMarkerLine).map(_.length).sum
val lineMapper = LineMapper(codeMarkerLine, userCodeOffset, adjustedSrc)
lineMapper.transform(tree)
}

}

class LineMapper(markerLine: Int, userCodeOffset: Int, adjustedSrc: SourceFile) extends tpd.TreeMapWithPreciseStatContexts() {

override def transform(tree: tpd.Tree)(using Context): tpd.Tree = {
val tree0 = super.transform(tree)
val pos = tree0.sourcePos
if pos.exists && pos.start >= userCodeOffset then
val tree1 = tree0.cloneIn(adjustedSrc).withSpan(pos.span.shift(-userCodeOffset))
// if tree1.show.toString == "???" then
// val pos1 = tree1.sourcePos
// sys.error(s"rewrote ??? at ${pos1.source}:${pos1.line + 1}:${pos1.column + 1} (sourced from ${markerLine + 2})")
tree1
else
tree0
}

}
25 changes: 25 additions & 0 deletions tests/plugins/run/scriptWrapper/Test_3.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@main def Test: Unit = {
val mainCls = Class.forName("foo_sc")
val mainMethod = mainCls.getMethod("main", classOf[Array[String]])
val stackTrace: Array[String] = {
try
mainMethod.invoke(null, Array.empty[String])
sys.error("Expected an exception")
catch
case e: java.lang.reflect.InvocationTargetException =>
val cause = e.getCause
if cause != null then
cause.getStackTrace.map(_.toString)
else
throw e
}

val expected = Set(
"foo_sc$.getRandom(foo_2.scala:3)", // adjusted line number (11 -> 3)
"foo_sc$.brokenRandom(foo_2.scala:5)", // adjusted line number (13 -> 5)
"foo_sc$.run(foo_2.scala:8)", // adjusted line number (16 -> 8)
)

val missing = expected -- stackTrace
assert(missing.isEmpty, s"Missing: $missing")
}
18 changes: 18 additions & 0 deletions tests/plugins/run/scriptWrapper/foo_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// generated code
// script: foo.sc
object foo_sc {
def main(args: Array[String]): Unit = {
run // assume some macro generates this by scanning for @entrypoint
}
//USER_SRC_FILE:./foo_original_2.scala
//USER_CODE_HERE
import framework.*

def getRandom: Int = brokenRandom // LINE 3;

def brokenRandom: Int = ??? // LINE 5;

@entrypoint
def run = println("Hello, here is a random number: " + getRandom) // LINE 8;
//END_USER_CODE_HERE
}
8 changes: 8 additions & 0 deletions tests/plugins/run/scriptWrapper/foo_original_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import framework.*

def getRandom: Int = brokenRandom // LINE 3;

def brokenRandom: Int = ??? // LINE 5;

@entrypoint
def run = println("Hello, here is a random number: " + getRandom) // LINE 8;
1 change: 1 addition & 0 deletions tests/plugins/run/scriptWrapper/plugin.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pluginClass=scriptWrapper.LineNumberPlugin