From 60d7e1c956bb573ca02ef91b0322c24e2d0aafe8 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Thu, 1 Oct 2026 11:33:57 +0200 Subject: [PATCH] Bump `java-class-name` to 0.2.0 --- .../build/internal/JavaParserProxy.scala | 8 +- .../internal/JavaParserProxyBinary.scala | 15 ++- .../build/internal/JavaParserProxyJvm.scala | 7 +- .../preprocessing/JavaPreprocessor.scala | 2 +- .../preprocessing/MarkdownPreprocessor.scala | 9 +- .../build/tests/JavaParserProxyTests.scala | 99 ++++++++++++++ .../build/tests/JavaPreprocessorTests.scala | 122 ++++++++++++++++++ .../markdown/MarkdownPreprocessorTests.scala | 92 +++++++++++++ .../scala/cli/integration/MarkdownTests.scala | 61 +++++++++ .../RunPipedSourcesTestDefinitions.scala | 57 ++++++++ .../RunSnippetTestDefinitions.scala | 59 +++++++++ project/deps/package.mill | 2 +- 12 files changed, 520 insertions(+), 13 deletions(-) create mode 100644 modules/build/src/test/scala/scala/build/tests/JavaParserProxyTests.scala create mode 100644 modules/build/src/test/scala/scala/build/tests/JavaPreprocessorTests.scala diff --git a/modules/build/src/main/scala/scala/build/internal/JavaParserProxy.scala b/modules/build/src/main/scala/scala/build/internal/JavaParserProxy.scala index 3873ea0fc2..8892b6bc71 100644 --- a/modules/build/src/main/scala/scala/build/internal/JavaParserProxy.scala +++ b/modules/build/src/main/scala/scala/build/internal/JavaParserProxy.scala @@ -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]] } diff --git a/modules/build/src/main/scala/scala/build/internal/JavaParserProxyBinary.scala b/modules/build/src/main/scala/scala/build/internal/JavaParserProxyBinary.scala index 4fa0c6e1ac..780d48788c 100644 --- a/modules/build/src/main/scala/scala/build/internal/JavaParserProxyBinary.scala +++ b/modules/build/src/main/scala/scala/build/internal/JavaParserProxyBinary.scala @@ -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) @@ -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) } diff --git a/modules/build/src/main/scala/scala/build/internal/JavaParserProxyJvm.scala b/modules/build/src/main/scala/scala/build/internal/JavaParserProxyJvm.scala index daa30bd709..2f5c3424b6 100644 --- a/modules/build/src/main/scala/scala/build/internal/JavaParserProxyJvm.scala +++ b/modules/build/src/main/scala/scala/build/internal/JavaParserProxyJvm.scala @@ -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)) } diff --git a/modules/build/src/main/scala/scala/build/preprocessing/JavaPreprocessor.scala b/modules/build/src/main/scala/scala/build/preprocessing/JavaPreprocessor.scala index 5bc9b6f246..e579920845 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/JavaPreprocessor.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/JavaPreprocessor.scala @@ -103,7 +103,7 @@ final case class JavaPreprocessor( logger, () => javaCommand() ) - .className(v.content) + .className(v.content, v.generatedSourceFileName) } val fileName = classNameOpt .map(_ + ".java") diff --git a/modules/build/src/main/scala/scala/build/preprocessing/MarkdownPreprocessor.scala b/modules/build/src/main/scala/scala/build/preprocessing/MarkdownPreprocessor.scala index 60dfd6f034..44f618e3d6 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/MarkdownPreprocessor.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/MarkdownPreprocessor.scala @@ -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" diff --git a/modules/build/src/test/scala/scala/build/tests/JavaParserProxyTests.scala b/modules/build/src/test/scala/scala/build/tests/JavaParserProxyTests.scala new file mode 100644 index 0000000000..84f871a2ee --- /dev/null +++ b/modules/build/src/test/scala/scala/build/tests/JavaParserProxyTests.scala @@ -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 + ) +} diff --git a/modules/build/src/test/scala/scala/build/tests/JavaPreprocessorTests.scala b/modules/build/src/test/scala/scala/build/tests/JavaPreprocessorTests.scala new file mode 100644 index 0000000000..020ddbf918 --- /dev/null +++ b/modules/build/src/test/scala/scala/build/tests/JavaPreprocessorTests.scala @@ -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" -> "-java-file", "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 + ) +} diff --git a/modules/build/src/test/scala/scala/build/tests/markdown/MarkdownPreprocessorTests.scala b/modules/build/src/test/scala/scala/build/tests/markdown/MarkdownPreprocessorTests.scala index 107f212971..a1342e07b1 100644 --- a/modules/build/src/test/scala/scala/build/tests/markdown/MarkdownPreprocessorTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/markdown/MarkdownPreprocessorTests.scala @@ -169,4 +169,96 @@ class MarkdownPreprocessorTests extends TestUtil.ScalaCliBuildSuite { expect(mainSources.inMemory.forall(!_.generatedRelPath.last.endsWith(".java"))) } } + + /** Checks the file name generated for a Java snippet in a Markdown file. + * + * @param expectedClassNameOpt + * the class name expected to be inferred, or `None` for the default file name + */ + private def javaSnippetFileNameTest( + description: String, + source: String, + expectedClassNameOpt: Option[String] + ): Unit = + test(s"a markdown file with a Java snippet: $description") { + TestInputs( + os.rel / "Example.md" -> + s"""# Example + | + |```java + |$source + |```""".stripMargin + ).withInputs { (root, inputs) => + val (crossSources, _) = + CrossSources.forInputs( + inputs, + preprocessors, + TestLogger(), + SuppressWarningOptions() + ).orThrow + + val mainSources = + crossSources.scopedSources(BuildOptions()).orThrow + .sources(Scope.Main, crossSources.sharedOptions(BuildOptions()), root, TestLogger()) + .orThrow + + val expectedFileName = expectedClassNameOpt.getOrElse("Example_md_snippet0") + expect(mainSources.inMemory.map(_.generatedRelPath) == + Seq(os.rel / s"$expectedFileName.java")) + } + } + + // https://github.com/VirtusLab/scala-cli/issues/4514 + javaSnippetFileNameTest( + description = "public enum", + source = "public enum Color { RED, GREEN }", + expectedClassNameOpt = Some("Color") + ) + javaSnippetFileNameTest( + 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 + javaSnippetFileNameTest( + 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 + javaSnippetFileNameTest( + 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 + ) + javaSnippetFileNameTest( + 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 + javaSnippetFileNameTest( + description = "compact source file", + source = """void main() { System.out.println("Hello"); }""", + expectedClassNameOpt = None + ) + javaSnippetFileNameTest( + 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 + ) } diff --git a/modules/integration/src/test/scala/scala/cli/integration/MarkdownTests.scala b/modules/integration/src/test/scala/scala/cli/integration/MarkdownTests.scala index f4541d80c2..3896054c07 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/MarkdownTests.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/MarkdownTests.scala @@ -324,6 +324,67 @@ class MarkdownTests extends ScalaCliSuite { } } + /** Runs a Markdown file with a Java snippet whose class name has to be inferred by + * java-class-name. + */ + private def javaSnippetTest( + description: String, + source: String, + expectedOutput: String, + jvmOptions: Seq[String] = Nil + ): Unit = + test(s"run a .md file with a java snippet: $description") { + TestInputs( + os.rel / "sample.md" -> + s"""# Sample + |```java + |$source + |``` + |""".stripMargin + ).fromRoot { root => + val res = os.proc(TestUtil.cli, "sample.md", jvmOptions) + .call(cwd = root, stderr = os.Pipe) + expect(res.out.trim() == expectedOutput) + // java-class-name used to report Java parser errors (e.g. for compact sources) to stderr + expect(!res.err.text().contains("-- Error:")) + } + } + + // https://github.com/VirtusLab/scala-cli/issues/4514 + javaSnippetTest( + description = "public enum with methods", + source = + "public enum Color { RED, GREEN; public static void main(String[] args) { System.out.println(RED); } }", + expectedOutput = "RED" + ) + // https://github.com/VirtusLab/scala-cli/issues/4516 + javaSnippetTest( + 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)); } }", + expectedOutput = "Point[x=1, y=2]" + ) + // https://github.com/VirtusLab/scala-cli/issues/4515 + javaSnippetTest( + description = "public class after a package-private one in a package", + source = + """package demo; + |class Helper { static int answer() { return 42; } } + |public class Main { public static void main(String[] args) { System.out.println(Helper.answer()); } } + |""".stripMargin, + expectedOutput = "42" + ) + // JEP 512: the implicit class of a compact source file is named after the generated source file + javaSnippetTest( + description = "compact source file with a public class before main", + source = + """public class Helper { static int answer() { return 42; } } + |void main() { System.out.println(getClass().getName() + ' ' + Helper.answer()); } + |""".stripMargin, + expectedOutput = "sample_md_snippet0 42", + jvmOptions = Seq("--jvm", TestUtil.jvmId(Constants.jep512MinJavaVersion)) + ) + test("run a .md file with scala and java snippets") { val expectedOutput = "Hello world" TestInputs( diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunPipedSourcesTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunPipedSourcesTestDefinitions.scala index 3b6b59cb9f..6e1fbb47a9 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunPipedSourcesTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunPipedSourcesTestDefinitions.scala @@ -83,6 +83,63 @@ trait RunPipedSourcesTestDefinitions { this: RunTestDefinitions => } } } + // java-class-name regressions are independent of the Scala version + if scalaVersionArgs.isEmpty then { + + /** Runs a Java source whose class name has to be inferred by java-class-name. The sources + * avoid string literals, so that they can be passed on the command line on all platforms. + */ + def pipedJavaSourceTest( + description: String, + source: String, + expectedOutput: String, + jvmOptions: Seq[String] = Nil + ): Unit = + test(s"piped Java source: $description") { + emptyInputs.fromRoot { root => + val res = os.proc(TestUtil.cli, "_.java", extraOptions, jvmOptions) + .call(cwd = root, stdin = source, stderr = os.Pipe) + expect(res.out.trim() == expectedOutput) + // java-class-name used to report Java parser errors (e.g. for compact sources) to stderr + expect(!res.err.text().contains("-- Error:")) + } + } + + // https://github.com/VirtusLab/scala-cli/issues/4514 + pipedJavaSourceTest( + description = "public enum with methods", + source = + "public enum Color { RED, GREEN; public static void main(String[] args) { System.out.println(RED); } }", + expectedOutput = "RED" + ) + // https://github.com/VirtusLab/scala-cli/issues/4516 + pipedJavaSourceTest( + 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)); } }", + expectedOutput = "Point[x=1, y=2]" + ) + // https://github.com/VirtusLab/scala-cli/issues/4515 + pipedJavaSourceTest( + description = "public class after a package-private one in a package", + source = + """package demo; + |class Helper { static int answer() { return 42; } } + |public class Main { public static void main(String[] args) { System.out.println(Helper.answer()); } } + |""".stripMargin, + expectedOutput = "42" + ) + // JEP 512: the implicit class of a compact source file is named after the generated source file + pipedJavaSourceTest( + description = "compact source file with a public class before main", + source = + """public class Helper { static int answer() { return 42; } } + |void main() { System.out.println(getClass().getName() + ' ' + Helper.answer()); } + |""".stripMargin, + expectedOutput = "stdin 42", + jvmOptions = Seq("--jvm", TestUtil.jvmId(Constants.jep512MinJavaVersion)) + ) + } test("Java code with multiple classes accepted as piped input") { val expectedOutput = "Hello" val pipedInput = diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunSnippetTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunSnippetTestDefinitions.scala index c072d5fb11..124d3d1150 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunSnippetTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunSnippetTestDefinitions.scala @@ -72,6 +72,65 @@ trait RunSnippetTestDefinitions { this: RunTestDefinitions => } } + // java-class-name regressions are independent of the Scala version + if scalaVersionArgs.isEmpty then { + + /** Runs a Java source whose class name has to be inferred by java-class-name. The sources avoid + * string literals, so that they can be passed on the command line on all platforms. + */ + def javaSnippetTest( + description: String, + source: String, + expectedOutput: String, + jvmOptions: Seq[String] = Nil + ): Unit = + test(s"java snippet: $description") { + emptyInputs.fromRoot { root => + val res = + os.proc(TestUtil.cli, "run", "--java-snippet", source, extraOptions, jvmOptions) + .call(cwd = root, stderr = os.Pipe) + expect(res.out.trim() == expectedOutput) + // java-class-name used to report Java parser errors (e.g. for compact sources) to stderr + expect(!res.err.text().contains("-- Error:")) + } + } + + // https://github.com/VirtusLab/scala-cli/issues/4514 + javaSnippetTest( + description = "public enum with methods", + source = + "public enum Color { RED, GREEN; public static void main(String[] args) { System.out.println(RED); } }", + expectedOutput = "RED" + ) + // https://github.com/VirtusLab/scala-cli/issues/4516 + javaSnippetTest( + 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)); } }", + expectedOutput = "Point[x=1, y=2]" + ) + // https://github.com/VirtusLab/scala-cli/issues/4515 + javaSnippetTest( + description = "public class after a package-private one in a package", + source = + """package demo; + |class Helper { static int answer() { return 42; } } + |public class Main { public static void main(String[] args) { System.out.println(Helper.answer()); } } + |""".stripMargin, + expectedOutput = "42" + ) + // JEP 512: the implicit class of a compact source file is named after the generated source file + javaSnippetTest( + description = "compact source file with a public class before main", + source = + """public class Helper { static int answer() { return 42; } } + |void main() { System.out.println(getClass().getName() + ' ' + Helper.answer()); } + |""".stripMargin, + expectedOutput = "java_snippet 42", + jvmOptions = Seq("--jvm", TestUtil.jvmId(Constants.jep512MinJavaVersion)) + ) + } + test("correctly run a markdown snippet") { emptyInputs.fromRoot { root => val msg = "Hello world" diff --git a/project/deps/package.mill b/project/deps/package.mill index de9bc12fa3..8c8fc16e77 100644 --- a/project/deps/package.mill +++ b/project/deps/package.mill @@ -164,7 +164,7 @@ object Deps { def signingCli = "0.4.0" def signingCliJvmVersion = Java.defaultJava def javaSemanticdb = "0.12.3" - def javaClassName = "0.1.9" + def javaClassName = "0.2.0" def bloop = "2.1.2" def bouncycastle = "1.86" def sbt1Version = "1.12.5"