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..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 @@ -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,9 +113,20 @@ class JavaClassNameTests extends munit.FunSuite { assertEquals(extractClassName("PrimRecord.java", content), "PrimRecord") } - // TODO: enums need initialized compiler definitions, the launcher crashes with an NPE + 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") + } + // https://github.com/VirtusLab/scala-cli/issues/4514 - test("enum".ignore) { + test("enum") { val content = """package a; | @@ -126,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; | @@ -141,9 +149,26 @@ class JavaClassNameTests extends munit.FunSuite { assertEquals(extractClassName("MethodEnum.java", content), "MethodEnum") } - // TODO: package-private classes aren't filtered out, "PackagePrivate" is printed + // 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") + } + // https://github.com/VirtusLab/scala-cli/issues/4515 - test("package-private class".ignore) { + test("package-private class") { val content = """package a; | @@ -152,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; | @@ -165,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 0ac62e4..1efe927 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,47 @@ 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() + + /** 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], @@ -37,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] = @@ -49,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 }) }