Skip to content

Commit

Permalink
using classpath: resolve relatively to the declaring file (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
mpollmeier authored Jun 17, 2024
1 parent 948f200 commit ba55c06
Show file tree
Hide file tree
Showing 7 changed files with 60 additions and 36 deletions.
9 changes: 7 additions & 2 deletions core/src/main/scala/replpp/UsingDirectives.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,13 @@ object UsingDirectives {
def findResolvers(lines: IterableOnce[String]): Seq[String] =
scanFor(ResolverDirective, lines)

def findClasspathEntries(lines: IterableOnce[String]): Seq[String] =
scanFor(ClasspathDirective, lines)
def findClasspathEntries(files: IterableOnce[Path]): Seq[Path] = {
for {
file <- files.iterator.toSeq
classpathEntry <- scanFor(ClasspathDirective, util.linesFromFile(file))
pathRelativeToDeclaringFile = file.getParent.resolve(classpathEntry)
} yield pathRelativeToDeclaringFile
}

private def scanFor(directive: String, lines: IterableOnce[String]): Seq[String] = {
lines
Expand Down
17 changes: 11 additions & 6 deletions core/src/main/scala/replpp/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,29 +41,34 @@ package object replpp {
compilerArgs.result()
}

/** recursively find all relevant source files from main script, global predef file,
* provided predef files, other scripts that were imported with `using file` directive */
def allSourceFiles(config: Config): Seq[Path] =
(allPredefFiles(config) ++ config.scriptFile).distinct.sorted

def allPredefFiles(config: Config): Seq[Path] = {
val allPredefFiles = mutable.Set.empty[Path]
allPredefFiles ++= config.predefFiles
globalPredefFileMaybe.foreach(allPredefFiles.addOne)

// the directly resolved predef files might reference additional files via `using` directive
val predefFilesDirect = allPredefFiles.toSet
predefFilesDirect.foreach { file =>
val importedFiles = UsingDirectives.findImportedFilesRecursively(file, visited = allPredefFiles.toSet)
predefFilesDirect.foreach { predefFile =>
val importedFiles = UsingDirectives.findImportedFilesRecursively(predefFile, visited = allPredefFiles.toSet)
allPredefFiles ++= importedFiles
}

// the script (if any) might also reference additional files via `using` directive
config.scriptFile.foreach { file =>
val importedFiles = UsingDirectives.findImportedFilesRecursively(file, visited = allPredefFiles.toSet)
config.scriptFile.foreach { scriptFile =>
val importedFiles = UsingDirectives.findImportedFilesRecursively(scriptFile, visited = allPredefFiles.toSet)
allPredefFiles ++= importedFiles
}

allPredefFiles.toSeq.sorted
}

def allPredefLines(config: Config): Seq[String] =
allPredefFiles(config).flatMap(linesFromFile)
def allSourceLines(config: Config): Seq[String] =
allSourceFiles(config).flatMap(linesFromFile)

/** precompile given predef files (if any) and update Config to include the results in the classpath */
def precompilePredefFiles(config: Config): Config = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package replpp.scripting

import replpp.{Config, allPredefFiles}
import replpp.{Config, allPredefFiles, allSourceFiles}

import java.nio.file.Files
import scala.util.{Failure, Success}
Expand Down
14 changes: 6 additions & 8 deletions core/src/main/scala/replpp/util/ClasspathHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,21 @@ object ClasspathHelper {
Using.resource(Source.fromFile(path.toFile))(_.getLines.toSeq)
}.getOrElse(Seq.empty)

val fromDependencies = dependencyArtifacts(config, scriptLines)
val fromDependencies = dependencyArtifacts(config)
fromDependencies.foreach(entries.addOne)
if (fromDependencies.nonEmpty && !quiet) {
println(s"resolved dependencies - adding ${fromDependencies.size} artifact(s) to classpath - to list them, enable verbose mode")
if (verboseEnabled(config)) fromDependencies.foreach(println)
}

val allLines = allPredefLines(config) ++ scriptLines
val additionalEntries = config.classpathConfig.additionalClasspathEntries ++ UsingDirectives.findClasspathEntries(allLines)
additionalEntries.map(Paths.get(_)).foreach(entries.addOne)
config.classpathConfig.additionalClasspathEntries.map(Paths.get(_)).foreach(entries.addOne)
UsingDirectives.findClasspathEntries(allSourceFiles(config)).iterator.foreach(entries.addOne)

entries.result().sorted
entries.result().distinct.sorted
}

private[util] def dependencyArtifacts(config: Config, scriptLines: Seq[String]): Seq[Path] = {
val allLines = allPredefLines(config) ++ scriptLines

private[util] def dependencyArtifacts(config: Config): Seq[Path] = {
val allLines = allSourceLines(config)
val resolvers = config.classpathConfig.resolvers ++ UsingDirectives.findResolvers(allLines)
val allDependencies = config.classpathConfig.dependencies ++ UsingDirectives.findDeclaredDependencies(allLines)
Dependencies.resolve(allDependencies, resolvers, verboseEnabled(config)).get
Expand Down
12 changes: 8 additions & 4 deletions core/src/test/scala/replpp/PredefTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class PredefTests extends AnyWordSpec with Matchers {
given Colors = Colors.BlackWhite

"recursively resolve `//> using file` directive" in {
val script = os.temp("val mainScript = 5")
val additionalScript1 = os.temp("val additionalScript1 = 10")
val additionalScript2 = os.temp("val additionalScript2 = 20")
val predefFile = os.temp(
Expand All @@ -15,17 +16,19 @@ class PredefTests extends AnyWordSpec with Matchers {
|//> using file $additionalScript2
|""".stripMargin)

allPredefLines(Config(predefFiles = Seq(predefFile.toNIO))).sorted shouldBe
allSourceLines(Config(predefFiles = Seq(predefFile.toNIO), scriptFile = Some(script.toNIO))).sorted shouldBe
Seq(
s"//> using file $additionalScript1",
s"//> using file $additionalScript2",
"val additionalScript1 = 10",
"val additionalScript2 = 20",
"val predefCode = 1"
"val mainScript = 5",
"val predefCode = 1",
).sorted
}

"recursively resolve `//> using file` directive - with recursive loops" in {
val script = os.temp("val mainScript = 5")
val additionalScript1 = os.temp(suffix = "-script1")
val additionalScript2 = os.temp(suffix = "-script2")

Expand All @@ -43,13 +46,14 @@ class PredefTests extends AnyWordSpec with Matchers {
|""".stripMargin)

// most importantly, this should not loop endlessly due to the recursive imports
allPredefLines(Config(predefFiles = Seq(predefFile.toNIO))).distinct.sorted shouldBe
allSourceLines(Config(predefFiles = Seq(predefFile.toNIO), scriptFile = Some(script.toNIO))).distinct.sorted shouldBe
Seq(
s"//> using file $additionalScript1",
s"//> using file $additionalScript2",
"val additionalScript1 = 10",
"val additionalScript2 = 20",
"val predefCode = 1"
"val mainScript = 5",
"val predefCode = 1",
).sorted
}
}
37 changes: 24 additions & 13 deletions core/src/test/scala/replpp/UsingDirectivesTests.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package replpp

import java.nio.file.Paths
import java.nio.file.{Path, Paths}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec

import scala.jdk.CollectionConverters.*

class UsingDirectivesTests extends AnyWordSpec with Matchers {
Expand Down Expand Up @@ -78,18 +79,28 @@ class UsingDirectivesTests extends AnyWordSpec with Matchers {
results should not contain "https://commented.out/repo"
}

"find declared classpath entries" in {
val source =
"""
|//> using classpath /path/to/cp1
|//> using classpath ../path/to/cp2
|// //> using classpath cp3
|""".stripMargin

val results = UsingDirectives.findClasspathEntries(source.linesIterator)
results should contain("/path/to/cp1")
results should contain("../path/to/cp2")
results should not contain "cp3"
if (scala.util.Properties.isWin) {
info("paths work differently on windows - ignoring some tests")
} else {
"find declared classpath entries" in {
val scriptFile = os.temp(
"""
|//> using classpath /path/to/cp1
|//> using classpath path/to/cp2
|//> using classpath ../path/to/cp3
|// //> using classpath cp4
|""".stripMargin
).toNIO

val scriptParentDir = scriptFile.getParent

val results = UsingDirectives.findClasspathEntries(Seq(scriptFile))
results should contain(Path.of("/path/to/cp1"))
results should contain(scriptParentDir.resolve("path/to/cp2"))
results should contain(scriptParentDir.resolve("../path/to/cp3"))
results should not contain Path.of("cp3")
results.size shouldBe 3 // just to triple check
}
}

}
5 changes: 3 additions & 2 deletions core/src/test/scala/replpp/util/ClasspathHelperTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ class ClasspathHelperTests extends AnyWordSpec with Matchers {
"org.scala-lang:scala-library:2.13.10",
"org.scala-lang::scala3-library:3.3.0",
)))
val deps = ClasspathHelper.dependencyArtifacts(config, scriptLines = Seq.empty)
val deps = ClasspathHelper.dependencyArtifacts(config)
deps.size shouldBe 2

assert(deps.find(_.endsWith("scala3-library_3-3.3.0.jar")).isDefined)
assert(deps.find(_.endsWith("scala-library-2.13.10.jar")).isDefined)
}

"declared in scriptFile" in {
val deps = ClasspathHelper.dependencyArtifacts(Config(), scriptLines = Seq("//> using dep com.michaelpollmeier::colordiff:0.36"))
val script = os.temp("//> using dep com.michaelpollmeier::colordiff:0.36")
val deps = ClasspathHelper.dependencyArtifacts(Config(scriptFile = Some(script.toNIO)))
deps.size shouldBe 4

assert(deps.find(_.endsWith("colordiff_3-0.36.jar")).isDefined)
Expand Down

0 comments on commit ba55c06

Please sign in to comment.