Skip to content

Commit a0b156b

Browse files
Add support for passing Java code by stdin (#956)
* Enable passing Java code through stdin * Parse the virtual file name automatically when Java code is passed through stdin * Add integration tests for running Java from stdin * Extract Java parsing to a Scala 3 module * fixup (fix Scala 3 / Scala 2.13 modules compat) * fixup (fmt) * fixup Extract Java parsing to a Scala 3 module (fmt) * move import Co-authored-by: Alexandre Archambault <[email protected]> Co-authored-by: Alexandre Archambault <[email protected]>
1 parent c4f3ab7 commit a0b156b

File tree

7 files changed

+137
-13
lines changed

7 files changed

+137
-13
lines changed

build.sc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ object `cli-options` extends CliOptions
6969
object `build-macros` extends Cross[BuildMacros](Scala.mainVersions: _*)
7070
object options extends Cross[Options](Scala.mainVersions: _*)
7171
object scalaparse extends ScalaParse
72+
object javaparse extends JavaParse
7273
object directives extends Cross[Directives](Scala.mainVersions: _*)
7374
object core extends Cross[Core](Scala.mainVersions: _*)
7475
object `build-module` extends Cross[Build](Scala.mainVersions: _*)
@@ -493,6 +494,34 @@ trait ScalaParse extends SbtModule with ScalaCliPublishModule with ScalaCliCompi
493494
def scalaVersion = Scala.scala213
494495
}
495496

497+
trait JavaParse extends SbtModule with ScalaCliPublishModule with ScalaCliCompile {
498+
def ivyDeps = super.ivyDeps() ++ Agg(Deps.scala3Compiler(scalaVersion()))
499+
500+
// pin scala3-library suffix, so that 2.13 modules can have us as moduleDep fine
501+
def mandatoryIvyDeps = T {
502+
super.mandatoryIvyDeps().map { dep =>
503+
val isScala3Lib =
504+
dep.dep.module.organization.value == "org.scala-lang" &&
505+
dep.dep.module.name.value == "scala3-library" &&
506+
(dep.cross match {
507+
case _: CrossVersion.Binary => true
508+
case _ => false
509+
})
510+
if (isScala3Lib)
511+
dep.copy(
512+
dep = dep.dep.withModule(
513+
dep.dep.module.withName(
514+
coursier.ModuleName(dep.dep.module.name.value + "_3")
515+
)
516+
),
517+
cross = CrossVersion.empty(dep.cross.platformed)
518+
)
519+
else dep
520+
}
521+
}
522+
def scalaVersion = Scala.scala3
523+
}
524+
496525
trait Scala3Runtime extends SbtModule with ScalaCliPublishModule with ScalaCliCompile {
497526
def ivyDeps = super.ivyDeps()
498527
def scalaVersion = Scala.scala3
@@ -528,6 +557,7 @@ class Build(val crossScalaVersion: String) extends BuildLikeModule {
528557
def moduleDeps = Seq(
529558
`options`(),
530559
scalaparse,
560+
javaparse,
531561
`directives`(),
532562
`scala-cli-bsp`,
533563
`test-runner`(),

modules/build/src/main/scala/scala/build/Inputs.scala

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ object Inputs {
185185
ScopePath(Left(source), subPath)
186186
}
187187

188+
sealed abstract class VirtualSourceFile extends Virtual {
189+
def isStdin: Boolean = source == "<stdin>"
190+
}
191+
188192
sealed trait SingleFile extends OnDisk with SingleElement
189193
sealed trait SourceFile extends SingleFile {
190194
def subPath: os.SubPath
@@ -210,9 +214,9 @@ object Inputs {
210214
final case class VirtualScript(content: Array[Byte], source: String, wrapperPath: os.SubPath)
211215
extends Virtual with AnyScalaFile with AnyScript
212216
final case class VirtualScalaFile(content: Array[Byte], source: String)
213-
extends Virtual with AnyScalaFile { def isStdin: Boolean = source == "<stdin>" }
217+
extends VirtualSourceFile with AnyScalaFile
214218
final case class VirtualJavaFile(content: Array[Byte], source: String)
215-
extends Virtual with Compiled
219+
extends VirtualSourceFile with Compiled
216220
final case class VirtualData(content: Array[Byte], source: String)
217221
extends Virtual
218222

@@ -336,6 +340,8 @@ object Inputs {
336340
val isStdin = (arg == "-.scala" || arg == "_" || arg == "_.scala") &&
337341
stdinOpt0.nonEmpty
338342
if (isStdin) Right(Seq(VirtualScalaFile(stdinOpt0.get, "<stdin>")))
343+
else if ((arg == "-.java" || arg == "_.java") && stdinOpt0.nonEmpty)
344+
Right(Seq(VirtualJavaFile(stdinOpt0.get, "<stdin>")))
339345
else if ((arg == "-" || arg == "-.sc" || arg == "_.sc") && stdinOpt0.nonEmpty)
340346
Right(Seq(VirtualScript(stdinOpt0.get, "stdin", os.sub / "stdin.sc")))
341347
else if (arg.endsWith(".zip") && os.exists(os.Path(arg, cwd))) {

modules/build/src/main/scala/scala/build/preprocessing/JavaPreprocessor.scala

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets
66

77
import scala.build.EitherCps.{either, value}
88
import scala.build.errors.BuildException
9+
import scala.build.internal.JavaParser
910
import scala.build.options.BuildRequirements
1011
import scala.build.preprocessing.ExtractedDirectives.from
1112
import scala.build.preprocessing.ScalaPreprocessor._
@@ -44,17 +45,25 @@ case object JavaPreprocessor extends Preprocessor {
4445
))
4546
})
4647
case v: Inputs.VirtualJavaFile =>
48+
val relPath =
49+
if (v.isStdin) {
50+
val fileName = JavaParser.parseRootPublicClassName(v.content).map(
51+
_ + ".java"
52+
).getOrElse("stdin.java")
53+
os.sub / fileName
54+
}
55+
else v.subPath
4756
val content = new String(v.content, StandardCharsets.UTF_8)
4857
val s = PreprocessedSource.InMemory(
49-
Left(v.source),
50-
v.subPath,
51-
content,
52-
0,
53-
None,
54-
None,
55-
Nil,
56-
None,
57-
v.scopePath
58+
originalPath = Left(v.source),
59+
relPath = relPath,
60+
code = content,
61+
ignoreLen = 0,
62+
options = None,
63+
requirements = None,
64+
scopedRequirements = Nil,
65+
mainClassOpt = None,
66+
scopePath = v.scopePath
5867
)
5968
Some(Right(Seq(s)))
6069

modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,46 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String])
729729
expect(output == expectedOutput)
730730
}
731731
}
732+
test("Java code accepted as piped input") {
733+
val expectedOutput = "Hello"
734+
val pipedInput =
735+
s"""public class Main {
736+
| public static void main(String[] args) {
737+
| System.out.println("$expectedOutput");
738+
| }
739+
|}
740+
|""".stripMargin
741+
emptyInputs.fromRoot { root =>
742+
val output = os.proc(TestUtil.cli, "_.java", extraOptions)
743+
.call(cwd = root, stdin = pipedInput)
744+
.out.text().trim
745+
expect(output == expectedOutput)
746+
}
747+
}
748+
test("Java code with multiple classes accepted as piped input") {
749+
val expectedOutput = "Hello"
750+
val pipedInput =
751+
s"""class OtherClass {
752+
| public String message;
753+
| public OtherClass(String message) {
754+
| this.message = message;
755+
| }
756+
|}
757+
|
758+
|public class Main {
759+
| public static void main(String[] args) {
760+
| OtherClass obj = new OtherClass("$expectedOutput");
761+
| System.out.println(obj.message);
762+
| }
763+
|}
764+
|""".stripMargin
765+
emptyInputs.fromRoot { root =>
766+
val output = os.proc(TestUtil.cli, "_.java", extraOptions)
767+
.call(cwd = root, stdin = pipedInput)
768+
.out.text().trim
769+
expect(output == expectedOutput)
770+
}
771+
}
732772
}
733773

734774
def fd(): Unit = {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package scala.build.internal
2+
3+
import dotty.tools.dotc.ast.{Trees, untpd}
4+
import dotty.tools.dotc.core.Contexts.{Context, ContextBase}
5+
import dotty.tools.dotc.parsing.JavaParsers.OutlineJavaParser
6+
import dotty.tools.dotc.util.SourceFile
7+
import dotty.tools.io.VirtualFile
8+
import dotty.tools.dotc.ast.untpd.{ModuleDef, PackageDef, Tree, TypeDef}
9+
import dotty.tools.dotc.core.Symbols.ClassSymbol
10+
import dotty.tools.dotc.core.{SymbolLoaders, Flags}
11+
import dotty.tools.dotc.ast.untpd.Modifiers
12+
import scala.io.Codec
13+
14+
object JavaParser {
15+
private def parseOutline(byteContent: Array[Byte]): untpd.Tree = {
16+
given Context = ContextBase().initialCtx.fresh
17+
val virtualFile = VirtualFile("placeholder.java", byteContent)
18+
val sourceFile = SourceFile(virtualFile, Codec.UTF8)
19+
val outlineParser = OutlineJavaParser(sourceFile)
20+
outlineParser.parse()
21+
}
22+
23+
extension(mdef: untpd.DefTree) {
24+
def nonPackagePrivate: Boolean = mdef.mods.privateWithin.toTermName.toString != "<empty>"
25+
def isPrivate: Boolean = mdef.mods.flags.is(Flags.Private)
26+
def isProtected: Boolean = mdef.mods.flags.is(Flags.Protected)
27+
}
28+
29+
def parseRootPublicClassName(byteContent: Array[Byte]): Option[String] =
30+
Option(parseOutline(byteContent))
31+
.flatMap {
32+
case pd: Trees.PackageDef[_] => Some(pd.stats)
33+
case _ => None
34+
}
35+
.flatMap(_.collectFirst {
36+
case mdef: ModuleDef if mdef.nonPackagePrivate && !mdef.isPrivate && !mdef.isProtected =>
37+
mdef.name.toString
38+
})
39+
}

project/deps.sc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ object Deps {
101101
def organizeImports = ivy"com.github.liancheng::organize-imports:0.5.0"
102102
def osLib = ivy"com.lihaoyi::os-lib:0.8.1"
103103
def pprint = ivy"com.lihaoyi::pprint:0.7.3"
104-
def scala3Compiler(sv: String) = ivy"org.scala-lang::scala3-compiler:$sv"
104+
def scala3Compiler(sv: String) = ivy"org.scala-lang:scala3-compiler_3:$sv"
105105
def scalaAsync = ivy"org.scala-lang.modules::scala-async:1.0.1".exclude("*" -> "*")
106106
def scalac(sv: String) = ivy"org.scala-lang:scala-compiler:$sv"
107107
def scalafmtCli = ivy"org.scalameta:scalafmt-cli_2.13:3.5.2"

project/settings.sc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -941,7 +941,7 @@ trait ScalaCliCrossSbtModule extends CrossSbtModule {
941941
val sv = scalaVersion()
942942
val isScala213 = sv.startsWith("2.13.")
943943
val extraOptions =
944-
if (isScala213) Seq("-Xsource:3")
944+
if (isScala213) Seq("-Xsource:3", "-Ytasty-reader")
945945
else Nil
946946
super.scalacOptions() ++ extraOptions
947947
}

0 commit comments

Comments
 (0)