diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala index 6ec896dcb200..aed58527602c 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala @@ -826,6 +826,26 @@ object JavaParsers { addCompanionObject(statics, cls) } + def unnamedClassDecl(priorTypes: List[Tree], firstMemberMods: Modifiers, start: Offset): List[Tree] = { + val name = source.name.replaceAll("\\.java$", "").nn.toTypeName + val (statics, body) = typeBodyDecls(CLASS, name, parentTParams = Nil, firstMemberMods = Some(firstMemberMods)) + + val priorStatics = priorTypes.map { + case t: (TypeDef | ModuleDef) => t.withMods(t.mods.withFlags(t.mods.flags | Flags.JavaStatic)) + case other => throw new Error(s"$other is neither TypeDef nor ModuleDef") + } + + val cls = atSpan(start, 0) { + TypeDef(name, makeTemplate( + parents = List(javaLangObject()), + stats = 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 @@ -899,13 +919,13 @@ object JavaParsers { defs } - def typeBodyDecls(parentToken: Int, parentName: Name, parentTParams: List[TypeDef]): (List[Tree], List[Tree]) = { + def typeBodyDecls(parentToken: Int, parentName: Name, parentTParams: List[TypeDef], firstMemberMods: Option[Modifiers] = None): (List[Tree], List[Tree]) = { val inInterface = definesInterface(parentToken) val statics = new ListBuffer[Tree] val members = new ListBuffer[Tree] while (in.token != RBRACE && in.token != EOF) { val start = in.offset - var mods = modifiers(inInterface) + var mods = (if (statics.isEmpty && members.isEmpty) firstMemberMods else None).getOrElse(modifiers(inInterface)) if (in.token == LBRACE) { skipAhead() // skip init block, we just assume we have seen only static accept(RBRACE) @@ -1067,16 +1087,35 @@ 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 + val cls = unnamedClassDecl(priorTypes = typesBuf.toList, firstMemberMods = mods, 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..f18dc6b8314f --- /dev/null +++ b/compiler/test/dotty/tools/dotc/parsing/JavaJep445ParserTest.scala @@ -0,0 +1,71 @@ +package dotty.tools.dotc.parsing + +import dotty.tools.DottyTest +import dotty.tools.dotc.ast.Trees.{Ident, PackageDef, TypeDef} +import dotty.tools.dotc.ast.untpd +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.{assertTrue, fail} +import org.junit.Test +import JavaParsers.JavaParser + +class JavaJep445ParserTest extends DottyTest { + + @Test def `the parser produces same trees for a class and an equivalent unnamed class` + : Unit = { + + val unnamed = JavaParser( + SourceFile.virtual( + "MyUnnamed.java", + s""" + |import some.pkg.*; + | + |@interface InnerAnnotation {} + | + |interface InnerInterface {} + | + |@Magic + |public volatile double d; + | + |void main() {} + | + |interface SecondInnerInterface {} + | + |""".stripMargin + ) + ).parse() + + val named = JavaParser( + SourceFile.virtual( + "SomeFile.java", + s""" + |import some.pkg.*; + | + |private final class MyUnnamed { + | + | @interface InnerAnnotation {} + | + | interface InnerInterface {} + | + | @Magic + | public volatile double d; + | + | void main() {} + | + | interface SecondInnerInterface {} + | + |} + |""".stripMargin + ) + ).parse() + + assertTrue( + "expected same trees for named and unnamed classes", + unnamed.sameTree(named) + ) + } +} 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() {}