Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ trait JavaParserProxy {
*
* @param content
* the Java source to extract a class name from
* @param sourceFileName
* the name of the file the source is going to be written to, which names the implicit class of
* compact source files (JEP 512)
* @return
* either some class name (if one was found) or none (if none was found), or a
* [[BuildException]]
*/
def className(content: Array[Byte]): Either[BuildException, Option[String]]
def className(
content: Array[Byte],
sourceFileName: String
): Either[BuildException, Option[String]]
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ class JavaParserProxyBinary(
() => javaCommand0.get()
)

def className(content: Array[Byte]): Either[BuildException, Option[String]] = either {
def className(
content: Array[Byte],
sourceFileName: String
): Either[BuildException, Option[String]] = either {

val platformSuffix = FetchExternalBinary.platformSuffix()
val version = javaClassNameVersionOpt.getOrElse(Constants.javaClassNameVersion)
Expand All @@ -60,16 +63,20 @@ class JavaParserProxyBinary(
val binary =
value(FetchExternalBinary.fetch(params, archiveCache, logger, javaCommand))

val source =
os.temp(content, suffix = ".java", perms = if (Properties.isWin) null else "rw-------")
// java-class-name names compact source files (JEP 512) after the source file, so the source
// has to be written under its actual file name
val sourceDir =
if Properties.isWin then os.temp.dir() else os.temp.dir(perms = "rwx------")
val source = sourceDir / sourceFileName
val command = binary.command
val output =
try {
os.write(source, content)
logger.debug(s"Running $command $source")
val res = os.proc(command, source).call()
res.out.trim()
}
finally os.remove(source)
finally os.remove.all(sourceDir)
if (output.isEmpty) None
else Some(output)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import scala.cli.javaclassname.JavaParser
* Should be used from Scala CLI when it's run on the JVM.
*/
class JavaParserProxyJvm extends JavaParserProxy {
override def className(content: Array[Byte]): Either[BuildException, Option[String]] =
Right(JavaParser.parseRootPublicClassName(content))
override def className(
content: Array[Byte],
sourceFileName: String
): Either[BuildException, Option[String]] =
Right(JavaParser.rootClassName(content, sourceFileName))
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ final case class JavaPreprocessor(
logger,
() => javaCommand()
)
.className(v.content)
.className(v.content, v.generatedSourceFileName)
}
val fileName = classNameOpt
.map(_ + ".java")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,12 @@ final case class MarkdownPreprocessor(
suppressWarningOptions: SuppressWarningOptions,
maybeRecoverOnError: BuildException => Option[BuildException]
)(using ScalaCliInvokeData): Either[BuildException, PreprocessedSource.InMemory] = either {
val classNameOpt = value {
javaParser.className(block.body.getBytes(StandardCharsets.UTF_8))
val mdBaseName = subPath.last.stripSuffix(".md")
val fallbackBaseName = s"${mdBaseName}_md_snippet$index"
val classNameOpt = value {
javaParser.className(block.body.getBytes(StandardCharsets.UTF_8), s"$fallbackBaseName.java")
}
val mdBaseName = subPath.last.stripSuffix(".md")
val baseName = classNameOpt.getOrElse(s"${mdBaseName}_md_snippet$index")
val baseName = classNameOpt.getOrElse(fallbackBaseName)
val javaFileName =
if isTest then s"$baseName.test.java"
else s"$baseName.java"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package scala.build.tests

import com.eed3si9n.expecty.Expecty.expect
import coursier.cache.ArchiveCache

import java.nio.charset.StandardCharsets

import scala.build.Ops.*
import scala.build.internal.{JavaParserProxy, JavaParserProxyBinary, JavaParserProxyJvm}

class JavaParserProxyTests extends TestUtil.ScalaCliBuildSuite {
// the binary proxy is what native launchers use, it downloads and runs a java-class-name launcher
private lazy val binaryProxy =
JavaParserProxyBinary(ArchiveCache(), None, TestLogger(), () => "java")

private val proxies: Seq[(String, () => JavaParserProxy)] =
Seq("JVM" -> (() => new JavaParserProxyJvm), "binary" -> (() => binaryProxy))

/** Checks the class name inferred by both proxies.
*
* @param expectedClassNameOpt
* the class name expected to be inferred, or `None` when the source file name should be used
*/
private def classNameTest(
description: String,
source: String,
expectedClassNameOpt: Option[String]
): Unit =
for (proxyName, proxy) <- proxies do
test(s"$proxyName: $description") {
val fallbackClassName = "stdin"
val classNameOpt = proxy()
.className(source.getBytes(StandardCharsets.UTF_8), s"$fallbackClassName.java")
.orThrow
expect(
classNameOpt.getOrElse(fallbackClassName) ==
expectedClassNameOpt.getOrElse(fallbackClassName)
)
}

// https://github.com/VirtusLab/scala-cli/issues/4514
classNameTest(
description = "public enum",
source = "public enum Color { RED, GREEN }",
expectedClassNameOpt = Some("Color")
)
classNameTest(
description = "public enum with methods",
source =
"""public enum Color {
| RED, GREEN;
| public static void main(String[] args) { System.out.println(RED); }
|}
|""".stripMargin,
expectedClassNameOpt = Some("Color")
)
// https://github.com/VirtusLab/scala-cli/issues/4516
classNameTest(
description = "public record with primitive components",
source =
"""public record Point(int x, int y) {
| public static void main(String[] args) { System.out.println(new Point(1, 2)); }
|}
|""".stripMargin,
expectedClassNameOpt = Some("Point")
)
// https://github.com/VirtusLab/scala-cli/issues/4515
classNameTest(
description = "package-private class in a package",
source =
"""package demo;
|class Main { public static void main(String[] args) { System.out.println("Hello"); } }
|""".stripMargin,
expectedClassNameOpt = None
)
classNameTest(
description = "public class after a package-private one in a package",
source =
"""package demo;
|class Helper { static String greet() { return "Hello"; } }
|public class Main { public static void main(String[] args) { System.out.println(Helper.greet()); } }
|""".stripMargin,
expectedClassNameOpt = Some("Main")
)
// JEP 512 compact source files: the implicit class is named after the source file
classNameTest(
description = "compact source file",
source = """void main() { System.out.println("Hello"); }""",
expectedClassNameOpt = None
)
classNameTest(
description = "compact source file with a public class before main",
source =
"""public class Helper { static String greet() { return "Hello"; } }
|void main() { System.out.println(Helper.greet()); }
|""".stripMargin,
expectedClassNameOpt = None
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package scala.build.tests

import com.eed3si9n.expecty.Expecty.expect
import coursier.cache.Cache.Fetch
import coursier.cache.{ArchiveCache, ArtifactError, Cache}
import coursier.util.{Artifact, EitherT, Task}

import java.io.File
import java.nio.charset.StandardCharsets

import scala.build.Ops.*
import scala.build.input.{ScalaCliInvokeData, VirtualJavaFile}
import scala.build.options.SuppressWarningOptions
import scala.build.preprocessing.{JavaPreprocessor, PreprocessedSource}
import scala.concurrent.ExecutionContext

class JavaPreprocessorTests extends TestUtil.ScalaCliBuildSuite {
private val javaPreprocessor = JavaPreprocessor(
archiveCache = ArchiveCache().withCache(
new Cache[Task] {
def fetch: Fetch[Task] = _ => sys.error("shouldn't be used")
def file(artifact: Artifact): EitherT[Task, ArtifactError, File] =
sys.error("shouldn't be used")
def ec: ExecutionContext = sys.error("shouldn't be used")
}
),
javaClassNameVersionOpt = None,
javaCommand = () => sys.error("shouldn't be used")
)

private def generatedRelPath(virtualJavaFile: VirtualJavaFile): os.RelPath =
javaPreprocessor.preprocess(
virtualJavaFile,
TestLogger(),
allowRestrictedFeatures = false,
suppressWarningOptions = SuppressWarningOptions()
)(using ScalaCliInvokeData.dummy).get.orThrow match {
case Seq(inMemory: PreprocessedSource.InMemory) => inMemory.relPath
case other => fail(s"Expected a single in-memory source, got $other")
}

/** Checks the file name generated for a Java source passed via stdin and as a snippet.
*
* @param expectedClassNameOpt
* the class name expected to be inferred, or `None` for the default file name
*/
private def generatedFileNameTest(
description: String,
source: String,
expectedClassNameOpt: Option[String]
): Unit =
for (inputKind, virtualSource) <-
Seq("stdin" -> "<stdin>-java-file", "snippet" -> "<snippet>-java-snippet")
do
test(s"$inputKind: $description") {
val virtualJavaFile =
VirtualJavaFile(source.getBytes(StandardCharsets.UTF_8), virtualSource)
val expectedFileName = expectedClassNameOpt
.map(_ + ".java")
.getOrElse(virtualJavaFile.generatedSourceFileName)
expect(generatedRelPath(virtualJavaFile) == os.rel / expectedFileName)
}

// https://github.com/VirtusLab/scala-cli/issues/4514
generatedFileNameTest(
description = "public enum",
source = "public enum Color { RED, GREEN }",
expectedClassNameOpt = Some("Color")
)
generatedFileNameTest(
description = "public enum with methods",
source =
"""public enum Color {
| RED, GREEN;
| public static void main(String[] args) { System.out.println(RED); }
|}
|""".stripMargin,
expectedClassNameOpt = Some("Color")
)
// https://github.com/VirtusLab/scala-cli/issues/4516
generatedFileNameTest(
description = "public record with primitive components",
source =
"""public record Point(int x, int y) {
| public static void main(String[] args) { System.out.println(new Point(1, 2)); }
|}
|""".stripMargin,
expectedClassNameOpt = Some("Point")
)
// https://github.com/VirtusLab/scala-cli/issues/4515
generatedFileNameTest(
description = "package-private class in a package",
source =
"""package demo;
|class Main { public static void main(String[] args) { System.out.println("Hello"); } }
|""".stripMargin,
expectedClassNameOpt = None
)
generatedFileNameTest(
description = "public class after a package-private one in a package",
source =
"""package demo;
|class Helper { static String greet() { return "Hello"; } }
|public class Main { public static void main(String[] args) { System.out.println(Helper.greet()); } }
|""".stripMargin,
expectedClassNameOpt = Some("Main")
)
// JEP 512 compact source files: the implicit class is named after the source file
generatedFileNameTest(
description = "compact source file",
source = """void main() { System.out.println("Hello"); }""",
expectedClassNameOpt = None
)
generatedFileNameTest(
description = "compact source file with a public class before main",
source =
"""public class Helper { static String greet() { return "Hello"; } }
|void main() { System.out.println(Helper.greet()); }
|""".stripMargin,
expectedClassNameOpt = None
)
}
Loading