From e9b364a97d3e17314ba5b90b816dcff307f9b39d Mon Sep 17 00:00:00 2001 From: "Chris (Krzysztof) Pado" Date: Sun, 29 Oct 2023 16:54:15 -0700 Subject: [PATCH] Java parser: add support for unnamed classes (JEP-445) Fixes #18584 --- .../tools/dotc/parsing/JavaParsers.scala | 46 ++++++++++- .../dotty/tools/dotc/CompilationTests.scala | 3 + .../dotc/parsing/JavaJep445ParserTest.scala | 82 +++++++++++++++++++ tests/pos-java21+/jep445/B.scala | 1 + tests/pos-java21+/jep445/UnnamedMainOnly.java | 1 + .../UnnamedStartsWithAnnotatedField.java | 5 ++ .../jep445/UnnamedStartsWithField.java | 9 ++ .../jep445/UnnamedStartsWithType.java | 4 + 8 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 compiler/test/dotty/tools/dotc/parsing/JavaJep445ParserTest.scala create mode 100644 tests/pos-java21+/jep445/B.scala create mode 100644 tests/pos-java21+/jep445/UnnamedMainOnly.java create mode 100644 tests/pos-java21+/jep445/UnnamedStartsWithAnnotatedField.java create mode 100644 tests/pos-java21+/jep445/UnnamedStartsWithField.java create mode 100644 tests/pos-java21+/jep445/UnnamedStartsWithType.java diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala index 6ec896dcb200..bdcf190f590c 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala @@ -826,6 +826,27 @@ object JavaParsers { addCompanionObject(statics, cls) } + def unnamedClassDecl(priorTypes: List[Tree], start: Offset): List[Tree] = { + val name = source.name.replaceAll("\\.java$", "").nn.toTypeName + val (statics, body) = typeBodyDecls(CLASS, name, Nil) + + val (priorStatics, priorBody) = priorTypes.partition { + case t: TypeDef => t.mods.is(Flags.JavaStatic) + case _: ModuleDef => true + case _ => false + } + + val cls = atSpan(start, 0) { + TypeDef(name, makeTemplate( + parents = Nil, + stats = priorBody ::: body, + tparams = Nil, + needsDummyConstr = true) + ).withMods(Modifiers(Flags.Private | Flags.Final)) + } + addCompanionObject(priorStatics ::: statics, cls) + } + def recordDecl(start: Offset, mods: Modifiers): List[Tree] = accept(RECORD) val nameOffset = in.offset @@ -1067,16 +1088,37 @@ object JavaParsers { val buf = new ListBuffer[Tree] while (in.token == IMPORT) buf ++= importDecl() + + val afterImports = in.offset + val typesBuf = new ListBuffer[Tree] + while (in.token != EOF && in.token != RBRACE) { while (in.token == SEMI) in.nextToken() if (in.token != EOF) { val start = in.offset val mods = modifiers(inInterface = false) adaptRecordIdentifier() // needed for typeDecl - buf ++= typeDecl(start, mods) + + in.token match { + case ENUM | INTERFACE | AT | CLASS | RECORD => typesBuf ++= typeDecl(start, mods) + case _ => + if (thisPackageName == tpnme.EMPTY_PACKAGE) { + // upon encountering non-types directly at a compilation unit level in an unnamed package, + // the entire compilation unit is treated as a JEP-445 unnamed class + //TODO support @annotated members of unnamed class + val cls = unnamedClassDecl(priorTypes = typesBuf.toList, start = afterImports) + typesBuf.clear() + typesBuf ++= cls + } else { + in.nextToken() + syntaxError(em"illegal start of type declaration", skipIt = true) + List(errorTypeTree) + } + } } } - val unit = atSpan(start) { PackageDef(pkg, buf.toList) } + + val unit = atSpan(start) { PackageDef(pkg, (buf ++ typesBuf).toList) } accept(EOF) unit match case PackageDef(Ident(nme.EMPTY_PACKAGE), Nil) => EmptyTree diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 2acb71ce62a8..88477c68b724 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -50,6 +50,9 @@ class CompilationTests { if scala.util.Properties.isJavaAtLeast("16") then tests ::= compileFilesInDir("tests/pos-java16+", defaultOptions.and("-Ysafe-init")) + if scala.util.Properties.isJavaAtLeast("21") then + tests ::= compileFilesInDir("tests/pos-java21+", defaultOptions.withJavacOnlyOptions("--enable-preview", "--release", "21")) + aggregateTests(tests*).checkCompile() } diff --git a/compiler/test/dotty/tools/dotc/parsing/JavaJep445ParserTest.scala b/compiler/test/dotty/tools/dotc/parsing/JavaJep445ParserTest.scala new file mode 100644 index 000000000000..c2bd1fa50532 --- /dev/null +++ b/compiler/test/dotty/tools/dotc/parsing/JavaJep445ParserTest.scala @@ -0,0 +1,82 @@ +package dotty.tools.dotc.parsing + +import dotty.tools.DottyTest +import dotty.tools.dotc.ast.Trees.{Ident, PackageDef, TypeDef} +import dotty.tools.dotc.ast.untpd.ModuleDef +import dotty.tools.dotc.core.Contexts.{Context, ContextBase} +import dotty.tools.dotc.core.StdNames.tpnme +import dotty.tools.dotc.printing.{PlainPrinter, Printer} +import dotty.tools.dotc.util.SourceFile +import dotty.tools.io.PlainFile +import org.junit.Assert.fail +import org.junit.Test + +class JavaJep445ParserTest extends DottyTest { + + @Test def `parses unnamed class`: Unit = { + val code = + s""" + |// hello + | + |import some.pkg.*; + | + |private volatile int x = 0; + |@Magic + |protected String s = "s"; + | + |void main() {} + |""".stripMargin + + val parser = + JavaParsers.JavaParser(SourceFile.virtual("MyUnnamed.java", code)) + val tree = parser.parse() + + println(tree.show) + + fail("TODO") + } + + @Test def `treats leading top-level types as nested types of unnamed class`: Unit = { + val code = + s""" + |// hello + | + |import some.pkg.*; + | + |interface Inner {} + | + |static class InnerStatic {} + | + |void main() {} + |""".stripMargin + + val parser = + JavaParsers.JavaParser(SourceFile.virtual("MyUnnamed.java", code)) + val tree = parser.parse() + + println(tree.show) + + fail("TODO") + } + + @Test def `treats leading top-level annotated vars as members of unnamed class`: Unit = { + val code = + s""" + | + |import some.pkg.*; + | + |@MyAnnotation + |int x = 0; + | + |void main() {} + |""".stripMargin + + val parser = + JavaParsers.JavaParser(SourceFile.virtual("MyUnnamed.java", code)) + val tree = parser.parse() + + println(tree.show) + + fail("TODO") + } +} diff --git a/tests/pos-java21+/jep445/B.scala b/tests/pos-java21+/jep445/B.scala new file mode 100644 index 000000000000..179f0d275843 --- /dev/null +++ b/tests/pos-java21+/jep445/B.scala @@ -0,0 +1 @@ +class B diff --git a/tests/pos-java21+/jep445/UnnamedMainOnly.java b/tests/pos-java21+/jep445/UnnamedMainOnly.java new file mode 100644 index 000000000000..ab73b3a234ab --- /dev/null +++ b/tests/pos-java21+/jep445/UnnamedMainOnly.java @@ -0,0 +1 @@ +void main() {} diff --git a/tests/pos-java21+/jep445/UnnamedStartsWithAnnotatedField.java b/tests/pos-java21+/jep445/UnnamedStartsWithAnnotatedField.java new file mode 100644 index 000000000000..ae7df0e58509 --- /dev/null +++ b/tests/pos-java21+/jep445/UnnamedStartsWithAnnotatedField.java @@ -0,0 +1,5 @@ + +@MyAnnotation +int myInt = 10; + +void main() {} diff --git a/tests/pos-java21+/jep445/UnnamedStartsWithField.java b/tests/pos-java21+/jep445/UnnamedStartsWithField.java new file mode 100644 index 000000000000..689d1af40966 --- /dev/null +++ b/tests/pos-java21+/jep445/UnnamedStartsWithField.java @@ -0,0 +1,9 @@ +private volatile int myInt = 10; + +String hello() { + return "hello"; +} + +interface Inner {} + +void main() {} diff --git a/tests/pos-java21+/jep445/UnnamedStartsWithType.java b/tests/pos-java21+/jep445/UnnamedStartsWithType.java new file mode 100644 index 000000000000..204b8533acd8 --- /dev/null +++ b/tests/pos-java21+/jep445/UnnamedStartsWithType.java @@ -0,0 +1,4 @@ + +class InnerOfUnnamed {} + +void main() {}