From e193ed59613dbf61ad8b5f4a1716c1ce206dc1b5 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Wed, 30 Sep 2026 15:16:24 +0200 Subject: [PATCH 1/3] fix: https://github.com/VirtusLab/scala-cli/issues/4516 --- .../javaclassname/JavaClassNameTests.scala | 16 ++++++++++--- .../scala/cli/javaclassname/JavaParser.scala | 23 ++++++++++++++++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/java-class-name-tests/src/test/scala/cli/javaclassname/JavaClassNameTests.scala b/java-class-name-tests/src/test/scala/cli/javaclassname/JavaClassNameTests.scala index 56fef65..ead0ccf 100644 --- a/java-class-name-tests/src/test/scala/cli/javaclassname/JavaClassNameTests.scala +++ b/java-class-name-tests/src/test/scala/cli/javaclassname/JavaClassNameTests.scala @@ -104,9 +104,7 @@ class JavaClassNameTests extends munit.FunSuite { assertEquals(extractClassName("RefRecord.java", content), "RefRecord") } - // TODO: primitive types need initialized compiler definitions, the launcher crashes with an NPE - // https://github.com/VirtusLab/scala-cli/issues/4516 - test("record with primitive components".ignore) { + test("record with primitive components") { val content = """package a; | @@ -115,6 +113,18 @@ class JavaClassNameTests extends munit.FunSuite { assertEquals(extractClassName("PrimRecord.java", content), "PrimRecord") } + test("record with all primitive kinds, arrays and varargs") { + val content = + """package a; + | + |public record AllPrims( + | byte a, short b, char c, int d, long e, float f, double g, boolean h, + | int[] i, double[][] j, long... k + |) {} + |""".stripMargin + assertEquals(extractClassName("AllPrims.java", content), "AllPrims") + } + // TODO: enums need initialized compiler definitions, the launcher crashes with an NPE // https://github.com/VirtusLab/scala-cli/issues/4514 test("enum".ignore) { diff --git a/java-class-name/src/scala/cli/javaclassname/JavaParser.scala b/java-class-name/src/scala/cli/javaclassname/JavaParser.scala index 0ac62e4..bb11b27 100644 --- a/java-class-name/src/scala/cli/javaclassname/JavaParser.scala +++ b/java-class-name/src/scala/cli/javaclassname/JavaParser.scala @@ -6,6 +6,7 @@ import dotty.tools.dotc.core.Contexts.{Context, ContextBase} import dotty.tools.dotc.core.Flags import dotty.tools.dotc.core.StdNames.tpnme import dotty.tools.dotc.parsing.JavaParsers.OutlineJavaParser +import dotty.tools.dotc.parsing.JavaTokens import dotty.tools.dotc.util.SourceFile import dotty.tools.io.VirtualFile @@ -13,13 +14,29 @@ import scala.io.Codec object JavaParser { - /** The stock parser emits typed trees for `java.lang.Object` and `Unit`, which require the - * compiler definitions to be initialized (and thus the Scala library on the classpath). We only - * need names and modifiers, so we fall back to untyped trees and skip the dummy constructors. + /** The stock parser emits typed trees for `java.lang.Object`, `Unit` and the Java primitive + * types, which require the compiler definitions to be initialized (and thus the Scala library on + * the classpath). We only need names and modifiers, so we fall back to untyped trees and skip + * the dummy constructors. */ private class UntypedOutlineJavaParser(source: SourceFile)(using Context) extends OutlineJavaParser(source) { override def ObjectTpt(): untpd.Tree = javaLangDot(tpnme.Object) + + /** Primitive types show up in record headers (and method signatures), e.g. `record R(int a)`. + * The stock implementation resolves them via `defn.IntType` & co, which crashes with an NPE + * without initialized definitions. We only need class names, so any untyped placeholder tree + * will do. Non-primitive tokens must still go through the stock syntax error reporting and + * recovery (`skip()`), otherwise malformed input could throw off brace balancing. + */ + override def basicType(): untpd.Tree = + if JavaTokens.primTypes.contains(in.token) then + atSpan(in.offset) { + in.nextToken() + ObjectTpt() + } + else super.basicType() + override def makeTemplate( parents: List[untpd.Tree], stats: List[untpd.Tree], From 6c47a715466a70ab257bbc31be4ed6d8eed03807 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Wed, 30 Sep 2026 15:25:53 +0200 Subject: [PATCH 2/3] fix: https://github.com/VirtusLab/scala-cli/issues/4514 --- .../javaclassname/JavaClassNameTests.scala | 24 +++++++++++++++---- .../scala/cli/javaclassname/JavaParser.scala | 18 ++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/java-class-name-tests/src/test/scala/cli/javaclassname/JavaClassNameTests.scala b/java-class-name-tests/src/test/scala/cli/javaclassname/JavaClassNameTests.scala index ead0ccf..0fc2be5 100644 --- a/java-class-name-tests/src/test/scala/cli/javaclassname/JavaClassNameTests.scala +++ b/java-class-name-tests/src/test/scala/cli/javaclassname/JavaClassNameTests.scala @@ -125,9 +125,8 @@ class JavaClassNameTests extends munit.FunSuite { assertEquals(extractClassName("AllPrims.java", content), "AllPrims") } - // TODO: enums need initialized compiler definitions, the launcher crashes with an NPE // https://github.com/VirtusLab/scala-cli/issues/4514 - test("enum".ignore) { + test("enum") { val content = """package a; | @@ -136,9 +135,8 @@ class JavaClassNameTests extends munit.FunSuite { assertEquals(extractClassName("SimpleEnum.java", content), "SimpleEnum") } - // TODO: enums need initialized compiler definitions, the launcher crashes with an NPE // https://github.com/VirtusLab/scala-cli/issues/4514 - test("enum with methods".ignore) { + test("enum with methods") { val content = """package a; | @@ -151,6 +149,24 @@ class JavaClassNameTests extends munit.FunSuite { assertEquals(extractClassName("MethodEnum.java", content), "MethodEnum") } + // https://github.com/VirtusLab/scala-cli/issues/4514 + test("enum with constructor args, constant bodies and interfaces") { + val content = + """package a; + | + |public enum FancyEnum implements Runnable, java.io.Serializable { + | A(1) { public void run() {} }, + | B(2, "b"); + | private final int n; + | FancyEnum(int n) { this(n, ""); } + | FancyEnum(int n, String s) { this.n = n; } + | public void run() {} + | public static void main(String[] args) { System.out.println(values().length); } + |} + |""".stripMargin + assertEquals(extractClassName("FancyEnum.java", content), "FancyEnum") + } + // TODO: package-private classes aren't filtered out, "PackagePrivate" is printed // https://github.com/VirtusLab/scala-cli/issues/4515 test("package-private class".ignore) { diff --git a/java-class-name/src/scala/cli/javaclassname/JavaParser.scala b/java-class-name/src/scala/cli/javaclassname/JavaParser.scala index bb11b27..f35880f 100644 --- a/java-class-name/src/scala/cli/javaclassname/JavaParser.scala +++ b/java-class-name/src/scala/cli/javaclassname/JavaParser.scala @@ -37,6 +37,24 @@ object JavaParser { } else super.basicType() + /** The stock `enumDecl` synthesizes `values()` / `valueOf(String)` via `defn.StringType` and a + * `java.lang.Enum[E]` parent, both of which crash with an NPE without initialized definitions. + * We only need the enum's name and modifiers, so we parse the header, let `typeBody` skip the + * body (constants included) and emit the plain class + companion pair like other declarations. + */ + override def enumDecl(start: Int, mods: untpd.Modifiers): List[untpd.Tree] = { + accept(JavaTokens.ENUM) + val nameOffset = in.offset + val name = identForType() + val interfaces = interfacesOpt() + val (statics, body) = typeBody(JavaTokens.ENUM, name) + val enumClass = atSpan(start, nameOffset) { + untpd.TypeDef(name, makeTemplate(interfaces, body, Nil, needsDummyConstr = false)) + .withMods(mods | Flags.JavaEnum) + } + addCompanionObject(statics, enumClass) + } + override def makeTemplate( parents: List[untpd.Tree], stats: List[untpd.Tree], From 7b41eb92f4b97f1a08692e4257624ebe3e99b34e Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Wed, 30 Sep 2026 15:32:09 +0200 Subject: [PATCH 3/3] fix: https://github.com/VirtusLab/scala-cli/issues/4515 --- .../javaclassname/JavaClassNameTests.scala | 33 ++++++++++++++++--- .../scala/cli/javaclassname/JavaParser.scala | 14 +++++--- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/java-class-name-tests/src/test/scala/cli/javaclassname/JavaClassNameTests.scala b/java-class-name-tests/src/test/scala/cli/javaclassname/JavaClassNameTests.scala index 0fc2be5..5843f21 100644 --- a/java-class-name-tests/src/test/scala/cli/javaclassname/JavaClassNameTests.scala +++ b/java-class-name-tests/src/test/scala/cli/javaclassname/JavaClassNameTests.scala @@ -167,9 +167,8 @@ class JavaClassNameTests extends munit.FunSuite { assertEquals(extractClassName("FancyEnum.java", content), "FancyEnum") } - // TODO: package-private classes aren't filtered out, "PackagePrivate" is printed // https://github.com/VirtusLab/scala-cli/issues/4515 - test("package-private class".ignore) { + test("package-private class") { val content = """package a; | @@ -178,9 +177,8 @@ class JavaClassNameTests extends munit.FunSuite { assertEquals(extractClassName("PackagePrivate.java", content), "") } - // TODO: package-private classes aren't filtered out, "Helper" is printed // https://github.com/VirtusLab/scala-cli/issues/4515 - test("public class after a package-private one".ignore) { + test("public class after a package-private one") { val content = """package a; | @@ -191,4 +189,31 @@ class JavaClassNameTests extends munit.FunSuite { assertEquals(extractClassName("Second.java", content), "Second") } + test("package-private interface, enum and record") { + val content = + """package a.b.c; + | + |interface HiddenIface {} + |enum HiddenEnum { A } + |record HiddenRecord(int x) {} + |""".stripMargin + assertEquals(extractClassName("HiddenIface.java", content), "") + } + + test("package-private class in the default package") { + val content = + """class DefaultPackagePrivate {} + |""".stripMargin + assertEquals(extractClassName("DefaultPackagePrivate.java", content), "") + } + + test("public class after a package-private one in the default package") { + val content = + """class Helper {} + | + |public class Main {} + |""".stripMargin + assertEquals(extractClassName("Main.java", content), "Main") + } + } diff --git a/java-class-name/src/scala/cli/javaclassname/JavaParser.scala b/java-class-name/src/scala/cli/javaclassname/JavaParser.scala index f35880f..1efe927 100644 --- a/java-class-name/src/scala/cli/javaclassname/JavaParser.scala +++ b/java-class-name/src/scala/cli/javaclassname/JavaParser.scala @@ -72,9 +72,14 @@ object JavaParser { } extension (mdef: untpd.DefTree) { - def nonPackagePrivate: Boolean = mdef.mods.privateWithin.toTermName.toString != "" - def isPrivate: Boolean = mdef.mods.flags.is(Flags.Private) - def isProtected: Boolean = mdef.mods.flags.is(Flags.Protected) + + /** The Java parser has no `Public` flag. Instead, package-private (and `protected`) members get + * the enclosing package recorded as `privateWithin` (`` for the default package), while + * `public` and `private` leave it empty. So `public` is: nothing in `privateWithin` and + * neither `private` nor `protected` set. + */ + def isPublic: Boolean = + mdef.mods.privateWithin.isEmpty && !mdef.mods.isOneOf(Flags.Private | Flags.Protected) } def parseRootPublicClassName(byteContent: Array[Byte]): Option[String] = @@ -84,7 +89,6 @@ object JavaParser { case _ => None } .flatMap(_.collectFirst { - case mdef: ModuleDef if mdef.nonPackagePrivate && !mdef.isPrivate && !mdef.isProtected => - mdef.name.toString + case mdef: ModuleDef if mdef.isPublic => mdef.name.toString }) }