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 @@ -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;
|
Expand All @@ -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;
|
Expand All @@ -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;
|
Expand All @@ -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;
|
Expand All @@ -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;
|
Expand All @@ -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")
}

}
55 changes: 47 additions & 8 deletions java-class-name/src/scala/cli/javaclassname/JavaParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,55 @@ 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

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],
Expand All @@ -37,9 +72,14 @@ object JavaParser {
}

extension (mdef: untpd.DefTree) {
def nonPackagePrivate: Boolean = mdef.mods.privateWithin.toTermName.toString != "<empty>"
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` (`<empty>` 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] =
Expand All @@ -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
})
}
Loading