Skip to content

Commit

Permalink
SI-11908: support JDK16 records in Java parser
Browse files Browse the repository at this point in the history
JDK16 introduced records (JEP 395) for reducing the boilerplate
associated with small immutable classes. This new construct
automatically

  * makes fields `private`/`final` and generates accessors for them
  * overrides `equals`/`hashCode`/`toString`
  * creates a `final` class that extends `java.lang.Record`

The details are in "8.10. Record Classes" of the Java language specification.

Fixes scala/bug#11908
  • Loading branch information
harpocrates committed Apr 24, 2021
1 parent d974e99 commit f7ae7af
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 12 deletions.
105 changes: 93 additions & 12 deletions src/compiler/scala/tools/nsc/javac/JavaParsers.scala
Expand Up @@ -118,6 +118,8 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {

def javaLangObject(): Tree = javaLangDot(tpnme.Object)

def javaLangRecord(): Tree = javaLangDot(tpnme.Record)

def arrayOf(tpt: Tree) =
AppliedTypeTree(scalaDot(tpnme.Array), List(tpt))

Expand Down Expand Up @@ -564,6 +566,16 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {

def definesInterface(token: Int) = token == INTERFACE || token == AT

/** If the next token is the identifier "record", convert it into a proper
* token. Technically, "record" is just a restricted identifier. However,
* once we've figured out that it is in a position where it identifies a
* "record" class, it is much more convenient to promote it to a token.
*/
def adaptRecordIdentifier(): Unit = {
if (in.token == IDENTIFIER && in.name.toString == "record")
in.token = RECORD
}

def termDecl(mods: Modifiers, parentToken: Int): List[Tree] = {
val inInterface = definesInterface(parentToken)
val tparams = if (in.token == LT) typeParams() else List()
Expand All @@ -587,6 +599,10 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {
DefDef(mods, nme.CONSTRUCTOR, tparams, List(vparams), TypeTree(), methodBody())
}
}
} else if (in.token == LBRACE && parentToken == RECORD) {
// compact constructor
methodBody()
List.empty
} else {
var mods1 = mods
if (mods hasFlag Flags.ABSTRACT) mods1 = mods &~ Flags.ABSTRACT | Flags.DEFERRED
Expand Down Expand Up @@ -721,11 +737,14 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {
}
}

def memberDecl(mods: Modifiers, parentToken: Int): List[Tree] = in.token match {
case CLASS | ENUM | INTERFACE | AT =>
typeDecl(if (definesInterface(parentToken)) mods | Flags.STATIC else mods)
case _ =>
termDecl(mods, parentToken)
def memberDecl(mods: Modifiers, parentToken: Int): List[Tree] = {
adaptRecordIdentifier()
in.token match {
case CLASS | ENUM | RECORD | INTERFACE | AT =>
typeDecl(if (definesInterface(parentToken)) mods | Flags.STATIC else mods)
case _ =>
termDecl(mods, parentToken)
}
}

def makeCompanionObject(cdef: ClassDef, statics: List[Tree]): Tree =
Expand Down Expand Up @@ -808,6 +827,61 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {
})
}

def recordDecl(mods: Modifiers): List[Tree] = {
accept(RECORD)
val pos = in.currentPos
val name = identForType()
val tparams = typeParams()
val header = formalParams()
val superclass = javaLangRecord()
val interfaces = interfacesOpt()
val (statics, body) = typeBody(RECORD, name)

// Records generate a canonical constructor and accessors, unless they are manually specified
var generateCanonicalCtor = true
var generateAccessors = header
.view
.map { case ValDef(_, name, tpt, _) => name -> tpt }
.toMap
for (DefDef(_, name, List(), List(params), tpt, _) <- body) {
if (name == nme.CONSTRUCTOR && params.size == header.size) {
val ctorParamsAreCanonical = params.lazyZip(header).forall {
case (ValDef(_, _, tpt1, _), ValDef(_, _, tpt2, _)) => tpt1 equalsStructure tpt2
case _ => false
}
if (ctorParamsAreCanonical) generateCanonicalCtor = false
} else if (generateAccessors.contains(name) && params.isEmpty) {
generateAccessors -= name
}
}

// Generate canonical constructor and accessors, if not already manually specified
val accessors = generateAccessors
.map { case (name, tpt) =>
DefDef(Modifiers(Flags.JAVA), name, List(), List(), tpt, blankExpr)
}
.toList
val canonicalCtor = Option.when(generateCanonicalCtor) {
DefDef(
Modifiers(Flags.JAVA),
nme.CONSTRUCTOR,
List(),
List(header),
TypeTree(),
blankExpr
)
}

addCompanionObject(statics, atPos(pos) {
ClassDef(
mods | Flags.FINAL,
name,
tparams,
makeTemplate(superclass :: interfaces, canonicalCtor.toList ++ accessors ++ body)
)
})
}

def interfaceDecl(mods: Modifiers): List[Tree] = {
accept(INTERFACE)
val pos = in.currentPos
Expand Down Expand Up @@ -847,7 +921,10 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {
} else if (in.token == SEMI) {
in.nextToken()
} else {
if (in.token == ENUM || definesInterface(in.token)) mods |= Flags.STATIC

// See "14.3. Local Class and Interface Declarations"
if (in.token == ENUM || in.token == RECORD || definesInterface(in.token))
mods |= Flags.STATIC
val decls = joinComment(memberDecl(mods, parentToken))

@tailrec
Expand Down Expand Up @@ -956,12 +1033,16 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {
(res, hasClassBody)
}

def typeDecl(mods: Modifiers): List[Tree] = in.token match {
case ENUM => joinComment(enumDecl(mods))
case INTERFACE => joinComment(interfaceDecl(mods))
case AT => annotationDecl(mods)
case CLASS => joinComment(classDecl(mods))
case _ => in.nextToken(); syntaxError("illegal start of type declaration", skipIt = true); List(errorTypeTree)
def typeDecl(mods: Modifiers): List[Tree] = {
adaptRecordIdentifier()
in.token match {
case ENUM => joinComment(enumDecl(mods))
case INTERFACE => joinComment(interfaceDecl(mods))
case AT => annotationDecl(mods)
case CLASS => joinComment(classDecl(mods))
case RECORD => joinComment(recordDecl(mods))
case _ => in.nextToken(); syntaxError("illegal start of type declaration", skipIt = true); List(errorTypeTree)
}
}

def tryLiteral(negate: Boolean = false): Option[Constant] = {
Expand Down
1 change: 1 addition & 0 deletions src/compiler/scala/tools/nsc/javac/JavaTokens.scala
Expand Up @@ -20,6 +20,7 @@ object JavaTokens extends ast.parser.CommonTokens {

/** identifiers */
final val IDENTIFIER = 10
final val RECORD = 12 // restricted identifier, so not lexed directly
def isIdentifier(code: Int) =
code == IDENTIFIER

Expand Down
1 change: 1 addition & 0 deletions src/reflect/scala/reflect/internal/StdNames.scala
Expand Up @@ -264,6 +264,7 @@ trait StdNames {
final val Object: NameType = nameType("Object")
final val PrefixType: NameType = nameType("PrefixType")
final val Product: NameType = nameType("Product")
final val Record: NameType = nameType("Record")
final val Serializable: NameType = nameType("Serializable")
final val Singleton: NameType = nameType("Singleton")
final val Throwable: NameType = nameType("Throwable")
Expand Down
55 changes: 55 additions & 0 deletions test/files/pos/t11908/C.scala
@@ -0,0 +1,55 @@
// javaVersion: 16+
object C {

def useR1 = {
// constructor signature
val r1 = new R1(123, "hello")

// accessors signature
val i: Int = r1.i
val s: String = r1.s

// method
val s2: String = r1.someMethod()

// supertype
val isRecord: java.lang.Record = r1

()
}

def useR2 = {
// constructor signature
val r2 = new R2(123, "hello")

// accessors signature
val i: Int = r2.i
val s: String = r2.s

// method
val i2: Int = r2.getInt

// supertype
val isIntLike: IntLike = r2
val isRecord: java.lang.Record = r2

()
}

def useR3 = {
// constructor signature
val r3 = new R3(123, 42L, "hi")
new R3("hi", 123)

// accessors signature
val i: Int = r3.i
val l: Long = r3.l
val s: String = r3.s

// method
val l2: Long = r3.l(43L, 44L)

// supertype
val isRecord: java.lang.Record = r3
}
}
4 changes: 4 additions & 0 deletions test/files/pos/t11908/IntLike.scala
@@ -0,0 +1,4 @@
// javaVersion: 16+
trait IntLike {
def getInt: Int
}
7 changes: 7 additions & 0 deletions test/files/pos/t11908/R1.java
@@ -0,0 +1,7 @@
// javaVersion: 16+
record R1(int i, String s) {

public String someMethod() {
return s + "!";
}
}
12 changes: 12 additions & 0 deletions test/files/pos/t11908/R2.java
@@ -0,0 +1,12 @@
// javaVersion: 16+
final record R2(int i, String s) implements IntLike {
public int getInt() {
return i;
}

// Canonical constructor
public R2(int i, String s) {
this.i = i;
this.s = s.intern();
}
}
23 changes: 23 additions & 0 deletions test/files/pos/t11908/R3.java
@@ -0,0 +1,23 @@
// javaVersion: 16+
public record R3(int i, long l, String s) {

// User-specified accessor
public int i() {
return i + 1; // evil >:)
}

// Not an accessor - too many parameters
public long l(long a1, long a2) {
return a1 + a2;
}

// Secondary constructor
public R3(String s, int i) {
this(i, 42L, s);
}

// Compact constructor
public R3 {
s = s.intern();
}
}

0 comments on commit f7ae7af

Please sign in to comment.