Skip to content

Commit f7ae7af

Browse files
committed
SI-11908: support JDK16 records in Java parser
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
1 parent d974e99 commit f7ae7af

File tree

8 files changed

+196
-12
lines changed

8 files changed

+196
-12
lines changed

src/compiler/scala/tools/nsc/javac/JavaParsers.scala

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {
118118

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

121+
def javaLangRecord(): Tree = javaLangDot(tpnme.Record)
122+
121123
def arrayOf(tpt: Tree) =
122124
AppliedTypeTree(scalaDot(tpnme.Array), List(tpt))
123125

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

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

569+
/** If the next token is the identifier "record", convert it into a proper
570+
* token. Technically, "record" is just a restricted identifier. However,
571+
* once we've figured out that it is in a position where it identifies a
572+
* "record" class, it is much more convenient to promote it to a token.
573+
*/
574+
def adaptRecordIdentifier(): Unit = {
575+
if (in.token == IDENTIFIER && in.name.toString == "record")
576+
in.token = RECORD
577+
}
578+
567579
def termDecl(mods: Modifiers, parentToken: Int): List[Tree] = {
568580
val inInterface = definesInterface(parentToken)
569581
val tparams = if (in.token == LT) typeParams() else List()
@@ -587,6 +599,10 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {
587599
DefDef(mods, nme.CONSTRUCTOR, tparams, List(vparams), TypeTree(), methodBody())
588600
}
589601
}
602+
} else if (in.token == LBRACE && parentToken == RECORD) {
603+
// compact constructor
604+
methodBody()
605+
List.empty
590606
} else {
591607
var mods1 = mods
592608
if (mods hasFlag Flags.ABSTRACT) mods1 = mods &~ Flags.ABSTRACT | Flags.DEFERRED
@@ -721,11 +737,14 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {
721737
}
722738
}
723739

724-
def memberDecl(mods: Modifiers, parentToken: Int): List[Tree] = in.token match {
725-
case CLASS | ENUM | INTERFACE | AT =>
726-
typeDecl(if (definesInterface(parentToken)) mods | Flags.STATIC else mods)
727-
case _ =>
728-
termDecl(mods, parentToken)
740+
def memberDecl(mods: Modifiers, parentToken: Int): List[Tree] = {
741+
adaptRecordIdentifier()
742+
in.token match {
743+
case CLASS | ENUM | RECORD | INTERFACE | AT =>
744+
typeDecl(if (definesInterface(parentToken)) mods | Flags.STATIC else mods)
745+
case _ =>
746+
termDecl(mods, parentToken)
747+
}
729748
}
730749

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

830+
def recordDecl(mods: Modifiers): List[Tree] = {
831+
accept(RECORD)
832+
val pos = in.currentPos
833+
val name = identForType()
834+
val tparams = typeParams()
835+
val header = formalParams()
836+
val superclass = javaLangRecord()
837+
val interfaces = interfacesOpt()
838+
val (statics, body) = typeBody(RECORD, name)
839+
840+
// Records generate a canonical constructor and accessors, unless they are manually specified
841+
var generateCanonicalCtor = true
842+
var generateAccessors = header
843+
.view
844+
.map { case ValDef(_, name, tpt, _) => name -> tpt }
845+
.toMap
846+
for (DefDef(_, name, List(), List(params), tpt, _) <- body) {
847+
if (name == nme.CONSTRUCTOR && params.size == header.size) {
848+
val ctorParamsAreCanonical = params.lazyZip(header).forall {
849+
case (ValDef(_, _, tpt1, _), ValDef(_, _, tpt2, _)) => tpt1 equalsStructure tpt2
850+
case _ => false
851+
}
852+
if (ctorParamsAreCanonical) generateCanonicalCtor = false
853+
} else if (generateAccessors.contains(name) && params.isEmpty) {
854+
generateAccessors -= name
855+
}
856+
}
857+
858+
// Generate canonical constructor and accessors, if not already manually specified
859+
val accessors = generateAccessors
860+
.map { case (name, tpt) =>
861+
DefDef(Modifiers(Flags.JAVA), name, List(), List(), tpt, blankExpr)
862+
}
863+
.toList
864+
val canonicalCtor = Option.when(generateCanonicalCtor) {
865+
DefDef(
866+
Modifiers(Flags.JAVA),
867+
nme.CONSTRUCTOR,
868+
List(),
869+
List(header),
870+
TypeTree(),
871+
blankExpr
872+
)
873+
}
874+
875+
addCompanionObject(statics, atPos(pos) {
876+
ClassDef(
877+
mods | Flags.FINAL,
878+
name,
879+
tparams,
880+
makeTemplate(superclass :: interfaces, canonicalCtor.toList ++ accessors ++ body)
881+
)
882+
})
883+
}
884+
811885
def interfaceDecl(mods: Modifiers): List[Tree] = {
812886
accept(INTERFACE)
813887
val pos = in.currentPos
@@ -847,7 +921,10 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {
847921
} else if (in.token == SEMI) {
848922
in.nextToken()
849923
} else {
850-
if (in.token == ENUM || definesInterface(in.token)) mods |= Flags.STATIC
924+
925+
// See "14.3. Local Class and Interface Declarations"
926+
if (in.token == ENUM || in.token == RECORD || definesInterface(in.token))
927+
mods |= Flags.STATIC
851928
val decls = joinComment(memberDecl(mods, parentToken))
852929

853930
@tailrec
@@ -956,12 +1033,16 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {
9561033
(res, hasClassBody)
9571034
}
9581035

959-
def typeDecl(mods: Modifiers): List[Tree] = in.token match {
960-
case ENUM => joinComment(enumDecl(mods))
961-
case INTERFACE => joinComment(interfaceDecl(mods))
962-
case AT => annotationDecl(mods)
963-
case CLASS => joinComment(classDecl(mods))
964-
case _ => in.nextToken(); syntaxError("illegal start of type declaration", skipIt = true); List(errorTypeTree)
1036+
def typeDecl(mods: Modifiers): List[Tree] = {
1037+
adaptRecordIdentifier()
1038+
in.token match {
1039+
case ENUM => joinComment(enumDecl(mods))
1040+
case INTERFACE => joinComment(interfaceDecl(mods))
1041+
case AT => annotationDecl(mods)
1042+
case CLASS => joinComment(classDecl(mods))
1043+
case RECORD => joinComment(recordDecl(mods))
1044+
case _ => in.nextToken(); syntaxError("illegal start of type declaration", skipIt = true); List(errorTypeTree)
1045+
}
9651046
}
9661047

9671048
def tryLiteral(negate: Boolean = false): Option[Constant] = {

src/compiler/scala/tools/nsc/javac/JavaTokens.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ object JavaTokens extends ast.parser.CommonTokens {
2020

2121
/** identifiers */
2222
final val IDENTIFIER = 10
23+
final val RECORD = 12 // restricted identifier, so not lexed directly
2324
def isIdentifier(code: Int) =
2425
code == IDENTIFIER
2526

src/reflect/scala/reflect/internal/StdNames.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ trait StdNames {
264264
final val Object: NameType = nameType("Object")
265265
final val PrefixType: NameType = nameType("PrefixType")
266266
final val Product: NameType = nameType("Product")
267+
final val Record: NameType = nameType("Record")
267268
final val Serializable: NameType = nameType("Serializable")
268269
final val Singleton: NameType = nameType("Singleton")
269270
final val Throwable: NameType = nameType("Throwable")

test/files/pos/t11908/C.scala

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// javaVersion: 16+
2+
object C {
3+
4+
def useR1 = {
5+
// constructor signature
6+
val r1 = new R1(123, "hello")
7+
8+
// accessors signature
9+
val i: Int = r1.i
10+
val s: String = r1.s
11+
12+
// method
13+
val s2: String = r1.someMethod()
14+
15+
// supertype
16+
val isRecord: java.lang.Record = r1
17+
18+
()
19+
}
20+
21+
def useR2 = {
22+
// constructor signature
23+
val r2 = new R2(123, "hello")
24+
25+
// accessors signature
26+
val i: Int = r2.i
27+
val s: String = r2.s
28+
29+
// method
30+
val i2: Int = r2.getInt
31+
32+
// supertype
33+
val isIntLike: IntLike = r2
34+
val isRecord: java.lang.Record = r2
35+
36+
()
37+
}
38+
39+
def useR3 = {
40+
// constructor signature
41+
val r3 = new R3(123, 42L, "hi")
42+
new R3("hi", 123)
43+
44+
// accessors signature
45+
val i: Int = r3.i
46+
val l: Long = r3.l
47+
val s: String = r3.s
48+
49+
// method
50+
val l2: Long = r3.l(43L, 44L)
51+
52+
// supertype
53+
val isRecord: java.lang.Record = r3
54+
}
55+
}

test/files/pos/t11908/IntLike.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// javaVersion: 16+
2+
trait IntLike {
3+
def getInt: Int
4+
}

test/files/pos/t11908/R1.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// javaVersion: 16+
2+
record R1(int i, String s) {
3+
4+
public String someMethod() {
5+
return s + "!";
6+
}
7+
}

test/files/pos/t11908/R2.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// javaVersion: 16+
2+
final record R2(int i, String s) implements IntLike {
3+
public int getInt() {
4+
return i;
5+
}
6+
7+
// Canonical constructor
8+
public R2(int i, String s) {
9+
this.i = i;
10+
this.s = s.intern();
11+
}
12+
}

test/files/pos/t11908/R3.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// javaVersion: 16+
2+
public record R3(int i, long l, String s) {
3+
4+
// User-specified accessor
5+
public int i() {
6+
return i + 1; // evil >:)
7+
}
8+
9+
// Not an accessor - too many parameters
10+
public long l(long a1, long a2) {
11+
return a1 + a2;
12+
}
13+
14+
// Secondary constructor
15+
public R3(String s, int i) {
16+
this(i, 42L, s);
17+
}
18+
19+
// Compact constructor
20+
public R3 {
21+
s = s.intern();
22+
}
23+
}

0 commit comments

Comments
 (0)