Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support JDK 16 records in Java sources #9551

Merged
merged 5 commits into from Jul 8, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
119 changes: 100 additions & 19 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 && rtptName != nme.EMPTY && 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,13 @@ 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] = {
in.token match {
case CLASS | ENUM | RECORD | INTERFACE | AT =>
typeDecl(mods)
case _ =>
termDecl(mods, parentToken)
lrytz marked this conversation as resolved.
Show resolved Hide resolved
}
}

def makeCompanionObject(cdef: ClassDef, statics: List[Tree]): Tree =
Expand Down Expand Up @@ -802,12 +820,67 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {
javaLangObject()
}
val interfaces = interfacesOpt()
val (statics, body) = typeBody(CLASS, name)
val (statics, body) = typeBody(CLASS)
addCompanionObject(statics, atPos(pos) {
ClassDef(mods, name, tparams, makeTemplate(superclass :: interfaces, body))
})
}

def recordDecl(mods: Modifiers): List[Tree] = {
harpocrates marked this conversation as resolved.
Show resolved Hide resolved
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)

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is equalsStructure the right way to check that the method signatures match?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is tricky.. The parser-only implementation breaks here:

public record Test(String s) {
  public Test(java.lang.String s) {
    this.s = s;
  }
}

Using it from Scala:

Test.scala:2: error: ambiguous reference to overloaded definition,
both constructor Test in class Test of type (s: String): Test
and  constructor Test in class Test of type (s: String): Test
match argument types (String)
  def t = new Test("")
          ^
1 error

Maybe it's good enough for a first implementation. But to do it right we have to compare the parameter types symbolically, which is a little tricky: it needs to be done early enough, so that type checking a new R() expression finds the constructor, but late enough so that symbolic information is available...

See here for an example:

// add the copy method to case classes; this needs to be done here, not in SyntheticMethods, because
// the namer phase must traverse this copy method to create default getters for its parameters.
// here, clazz is the ClassSymbol of the case class (not the module). (!clazz.hasModuleFlag) excludes
// the moduleClass symbol of the companion object when the companion is a "case object".
if (clazz.isCaseClass && !clazz.hasModuleFlag) {
val modClass = companionSymbolOf(clazz, context).moduleClass
modClass.attachments.get[ClassForCaseCompanionAttachment] foreach { cma =>
val cdef = cma.caseClass
def hasCopy = (decls containsName nme.copy) || parents.exists(_.member(nme.copy).exists)
// scala/bug#5956 needs (cdef.symbol == clazz): there can be multiple class symbols with the same name
if (cdef.symbol == clazz && !hasCopy)
addCopyMethod(cdef, templateNamer)
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. This is exactly what I was worried about. Thank you for the pointer to similar code.

I'm a little concerned that the perfect solution won't be possible, but I'll try. If it isn't, I think we should lean towards assuming the constructor is different. It would be undesirable to assume the constructor is the same in a case like the following:

public record Test(String s) {
  public Test(some.other.fully.qualified.String s) {
    this.s = s;
  }
}

Unlike the case where we incorrectly mark String and java.lang.String as different, incorrectly assuming String and some.other.fully.qualified.String are the same isn't fixable from the Java source without renaming some.other.fully.qualified.String (something which might not even be possible if it comes from another package).

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, annots)) =>
DefDef(Modifiers(Flags.JAVA) withAnnotations annots, name, List(), List(), tpt.duplicate, blankExpr)
}
.toList
val canonicalCtor = Option.when(generateCanonicalCtor) {
DefDef(
mods,
nme.CONSTRUCTOR,
List(),
List(header.map(_.duplicate)),
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 All @@ -820,22 +893,22 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {
} else {
List(javaLangObject())
}
val (statics, body) = typeBody(INTERFACE, name)
val (statics, body) = typeBody(INTERFACE)
addCompanionObject(statics, atPos(pos) {
ClassDef(mods | Flags.TRAIT | Flags.INTERFACE | Flags.ABSTRACT,
name, tparams,
makeTemplate(parents, body))
})
}

def typeBody(leadingToken: Int, parentName: Name): (List[Tree], List[Tree]) = {
def typeBody(leadingToken: Int): (List[Tree], List[Tree]) = {
accept(LBRACE)
val defs = typeBodyDecls(leadingToken, parentName)
val defs = typeBodyDecls(leadingToken)
accept(RBRACE)
defs
}

def typeBodyDecls(parentToken: Int, parentName: Name): (List[Tree], List[Tree]) = {
def typeBodyDecls(parentToken: Int): (List[Tree], List[Tree]) = {
val inInterface = definesInterface(parentToken)
val statics = new ListBuffer[Tree]
val members = new ListBuffer[Tree]
Expand All @@ -847,7 +920,11 @@ 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"
adaptRecordIdentifier()
if (in.token == ENUM || in.token == RECORD || definesInterface(in.token))
mods |= Flags.STATIC
val decls = joinComment(memberDecl(mods, parentToken))

@tailrec
Expand All @@ -871,7 +948,7 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {
accept(INTERFACE)
val pos = in.currentPos
val name = identForType()
val (statics, body) = typeBody(AT, name)
val (statics, body) = typeBody(AT)
val templ = makeTemplate(annotationParents, body)
addCompanionObject(statics, atPos(pos) {
import Flags._
Expand Down Expand Up @@ -908,7 +985,7 @@ trait JavaParsers extends ast.parser.ParsersCommon with JavaScanners {
val (statics, body) =
if (in.token == SEMI) {
in.nextToken()
typeBodyDecls(ENUM, name)
typeBodyDecls(ENUM)
} else {
(List(), List())
}
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.R(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) {
harpocrates marked this conversation as resolved.
Show resolved Hide resolved

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

// Canonical constructor
public R(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();
}
}