Skip to content

Commit

Permalink
Java parser: add support for unnamed classes (JEP-445)
Browse files Browse the repository at this point in the history
Fixes #18584
  • Loading branch information
povder committed Nov 5, 2023
1 parent c0eae68 commit 01540b0
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 4 deletions.
47 changes: 43 additions & 4 deletions compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions compiler/test/dotty/tools/dotc/CompilationTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
71 changes: 71 additions & 0 deletions compiler/test/dotty/tools/dotc/parsing/JavaJep445ParserTest.scala
Original file line number Diff line number Diff line change
@@ -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)
)
}
}
1 change: 1 addition & 0 deletions tests/pos-java21+/jep445/B.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
class B
1 change: 1 addition & 0 deletions tests/pos-java21+/jep445/UnnamedMainOnly.java
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
void main() {}
5 changes: 5 additions & 0 deletions tests/pos-java21+/jep445/UnnamedStartsWithAnnotatedField.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

@MyAnnotation
int myInt = 10;

void main() {}
9 changes: 9 additions & 0 deletions tests/pos-java21+/jep445/UnnamedStartsWithField.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
private volatile int myInt = 10;

String hello() {
return "hello";
}

interface Inner {}

void main() {}
4 changes: 4 additions & 0 deletions tests/pos-java21+/jep445/UnnamedStartsWithType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

class InnerOfUnnamed {}

void main() {}

0 comments on commit 01540b0

Please sign in to comment.