From 52412e1f6303ba42ad00a05ee1b9b9c672b4dc3d Mon Sep 17 00:00:00 2001 From: Alec Theriault Date: Wed, 24 Mar 2021 08:08:30 -0700 Subject: [PATCH] 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 --- .../scala/tools/nsc/javac/JavaParsers.scala | 105 ++++++++++++++++-- .../scala/tools/nsc/javac/JavaTokens.scala | 1 + .../scala/reflect/internal/StdNames.scala | 1 + test/files/pos/t11908/C.scala | 54 +++++++++ test/files/pos/t11908/IntLike.scala | 3 + test/files/pos/t11908/R1.java | 6 + test/files/pos/t11908/R2.java | 11 ++ test/files/pos/t11908/R3.java | 22 ++++ 8 files changed, 191 insertions(+), 12 deletions(-) create mode 100644 test/files/pos/t11908/C.scala create mode 100644 test/files/pos/t11908/IntLike.scala create mode 100644 test/files/pos/t11908/R1.java create mode 100644 test/files/pos/t11908/R2.java create mode 100644 test/files/pos/t11908/R3.java diff --git a/src/compiler/scala/tools/nsc/javac/JavaParsers.scala b/src/compiler/scala/tools/nsc/javac/JavaParsers.scala index f2b820256630..c1d1b8924dbb 100644 --- a/src/compiler/scala/tools/nsc/javac/JavaParsers.scala +++ b/src/compiler/scala/tools/nsc/javac/JavaParsers.scala @@ -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)) @@ -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() @@ -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 @@ -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 = @@ -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 @@ -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 @@ -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] = { diff --git a/src/compiler/scala/tools/nsc/javac/JavaTokens.scala b/src/compiler/scala/tools/nsc/javac/JavaTokens.scala index 855fe19e6706..a124d1b90aaa 100644 --- a/src/compiler/scala/tools/nsc/javac/JavaTokens.scala +++ b/src/compiler/scala/tools/nsc/javac/JavaTokens.scala @@ -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 diff --git a/src/reflect/scala/reflect/internal/StdNames.scala b/src/reflect/scala/reflect/internal/StdNames.scala index 0c550505f360..474c588598a4 100644 --- a/src/reflect/scala/reflect/internal/StdNames.scala +++ b/src/reflect/scala/reflect/internal/StdNames.scala @@ -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") diff --git a/test/files/pos/t11908/C.scala b/test/files/pos/t11908/C.scala new file mode 100644 index 000000000000..ea4718011fe9 --- /dev/null +++ b/test/files/pos/t11908/C.scala @@ -0,0 +1,54 @@ +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 + } +} diff --git a/test/files/pos/t11908/IntLike.scala b/test/files/pos/t11908/IntLike.scala new file mode 100644 index 000000000000..e64274c70c8c --- /dev/null +++ b/test/files/pos/t11908/IntLike.scala @@ -0,0 +1,3 @@ +trait IntLike { + def getInt: Int +} diff --git a/test/files/pos/t11908/R1.java b/test/files/pos/t11908/R1.java new file mode 100644 index 000000000000..5a8d54480aa9 --- /dev/null +++ b/test/files/pos/t11908/R1.java @@ -0,0 +1,6 @@ +record R1(int i, String s) { + + public String someMethod() { + return s + "!"; + } +} diff --git a/test/files/pos/t11908/R2.java b/test/files/pos/t11908/R2.java new file mode 100644 index 000000000000..3f904febf603 --- /dev/null +++ b/test/files/pos/t11908/R2.java @@ -0,0 +1,11 @@ +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(); + } +} diff --git a/test/files/pos/t11908/R3.java b/test/files/pos/t11908/R3.java new file mode 100644 index 000000000000..6a93d49ef100 --- /dev/null +++ b/test/files/pos/t11908/R3.java @@ -0,0 +1,22 @@ +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(); + } +}