Skip to content

Commit 1af1f46

Browse files
Merge pull request #9487 from TheElectronWill/prevent-extend-java-enum
Fix #7499: Prevent extending java.lang.Enum except from an enum
2 parents 24b75d7 + 72d9322 commit 1af1f46

File tree

10 files changed

+73
-8
lines changed

10 files changed

+73
-8
lines changed

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -585,13 +585,18 @@ class Definitions {
585585

586586
@tu lazy val JavaEnumClass: ClassSymbol = {
587587
val cls = requiredClass("java.lang.Enum")
588+
// jl.Enum has a single constructor protected(name: String, ordinal: Int).
589+
// We remove the arguments from the primary constructor, and enter
590+
// a new constructor symbol with 2 arguments, so that both
591+
// `X extends jl.Enum[X]` and `X extends jl.Enum[X](name, ordinal)`
592+
// pass typer and go through jl.Enum-specific checks in RefChecks.
588593
cls.infoOrCompleter match {
589594
case completer: ClassfileLoader =>
590595
cls.info = new ClassfileLoader(completer.classfile) {
591596
override def complete(root: SymDenotation)(using Context): Unit = {
592597
super.complete(root)
593598
val constr = cls.primaryConstructor
594-
val newInfo = constr.info match {
599+
val noArgInfo = constr.info match {
595600
case info: PolyType =>
596601
info.resType match {
597602
case meth: MethodType =>
@@ -600,7 +605,8 @@ class Definitions {
600605
paramNames = Nil, paramInfos = Nil))
601606
}
602607
}
603-
constr.info = newInfo
608+
val argConstr = constr.copy().entered
609+
constr.info = noArgInfo
604610
constr.termRef.recomputeDenot()
605611
}
606612
}

compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ enum ErrorMessageID extends java.lang.Enum[ErrorMessageID] {
164164
UnexpectedPatternForSummonFromID,
165165
AnonymousInstanceCannotBeEmptyID,
166166
TypeSpliceInValPatternID,
167-
ModifierNotAllowedForDefinitionID
167+
ModifierNotAllowedForDefinitionID,
168+
CannotExtendJavaEnumID
168169

169170
def errorNumber = ordinal - 2
170171
}

compiler/src/dotty/tools/dotc/reporting/messages.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,6 +1490,12 @@ import ast.tpd
14901490
|"""
14911491
}
14921492

1493+
class CannotExtendJavaEnum(sym: Symbol)(using Context)
1494+
extends SyntaxMsg(CannotExtendJavaEnumID) {
1495+
def msg = em"""$sym cannot extend ${hl("java.lang.Enum")}: only enums defined with the ${hl("enum")} syntax can"""
1496+
def explain = ""
1497+
}
1498+
14931499
class CannotHaveSameNameAs(sym: Symbol, cls: Symbol, reason: CannotHaveSameNameAs.Reason)(using Context)
14941500
extends SyntaxMsg(CannotHaveSameNameAsID) {
14951501
import CannotHaveSameNameAs._

compiler/src/dotty/tools/dotc/transform/CompleteJavaEnums.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ class CompleteJavaEnums extends MiniPhase with InfoTransformer { thisPhase =>
7373
*/
7474
private def addEnumConstrArgs(targetCls: Symbol, parents: List[Tree], args: List[Tree])(using Context): List[Tree] =
7575
parents.map {
76-
case app @ Apply(fn, args0) if fn.symbol.owner == targetCls => cpy.Apply(app)(fn, args0 ++ args)
76+
case app @ Apply(fn, args0) if fn.symbol.owner == targetCls =>
77+
if args0.nonEmpty && targetCls == defn.JavaEnumClass then
78+
report.error("the constructor of java.lang.Enum cannot be called explicitly", app.sourcePos)
79+
cpy.Apply(app)(fn, args0 ++ args)
7780
case p => p
7881
}
7982

compiler/src/dotty/tools/dotc/typer/RefChecks.scala

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import scala.util.{Try, Failure, Success}
1717
import config.{ScalaVersion, NoScalaVersion}
1818
import Decorators._
1919
import typer.ErrorReporting._
20-
import config.Feature.warnOnMigration
20+
import config.Feature.{warnOnMigration, migrateTo3}
2121
import reporting._
2222

2323
object RefChecks {
@@ -88,8 +88,9 @@ object RefChecks {
8888
cls.thisType
8989
}
9090

91-
/** Check that self type of this class conforms to self types of parents.
92-
* and required classes.
91+
/** Check that self type of this class conforms to self types of parents
92+
* and required classes. Also check that only `enum` constructs extend
93+
* `java.lang.Enum`.
9394
*/
9495
private def checkParents(cls: Symbol)(using Context): Unit = cls.info match {
9596
case cinfo: ClassInfo =>
@@ -99,10 +100,19 @@ object RefChecks {
99100
report.error(DoesNotConformToSelfType(category, cinfo.selfType, cls, otherSelf, relation, other.classSymbol),
100101
cls.sourcePos)
101102
}
102-
for (parent <- cinfo.classParents)
103+
val parents = cinfo.classParents
104+
for (parent <- parents)
103105
checkSelfConforms(parent.classSymbol.asClass, "illegal inheritance", "parent")
104106
for (reqd <- cinfo.cls.givenSelfType.classSymbols)
105107
checkSelfConforms(reqd, "missing requirement", "required")
108+
109+
// Prevent wrong `extends` of java.lang.Enum
110+
if !migrateTo3 &&
111+
!cls.isOneOf(Enum | Trait) &&
112+
parents.exists(_.classSymbol == defn.JavaEnumClass)
113+
then
114+
report.error(CannotExtendJavaEnum(cls), cls.sourcePos)
115+
106116
case _ =>
107117
}
108118

compiler/test/dotty/tools/dotc/CompilationTests.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class CompilationTests extends ParallelTesting {
6666
compileFile("tests/pos-custom-args/i5498-postfixOps.scala", defaultOptions withoutLanguageFeature "postfixOps"),
6767
compileFile("tests/pos-custom-args/i8875.scala", defaultOptions.and("-Xprint:getters")),
6868
compileFile("tests/pos-custom-args/i9267.scala", defaultOptions.and("-Ystop-after:erasure")),
69+
compileFile("tests/pos-special/extend-java-enum.scala", defaultOptions.and("-source", "3.0-migration")),
6970
).checkCompile()
7071
}
7172

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
enum E extends java.lang.Enum[E]("name", 0) { // error
2+
case A, B
3+
}

tests/neg/extend-java-enum.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import java.{lang => jl}
2+
3+
class C1 extends jl.Enum[C1] // error: class C1 cannot extend java.lang.Enum
4+
5+
class C2(name: String, ordinal: Int) extends jl.Enum[C2](name, ordinal) // error: class C2 cannot extend java.lang.Enum
6+
7+
trait T extends jl.Enum[T] // ok
8+
9+
class C3 extends T // error: class C3 cannot extend java.lang.Enum
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import java.{lang => jl}
2+
3+
final class ConfigSyntax private (name: String, ordinal: Int)
4+
extends jl.Enum[ConfigSyntax](name, ordinal)
5+
6+
object ConfigSyntax {
7+
8+
final val JSON = new ConfigSyntax("JSON", 0)
9+
final val CONF = new ConfigSyntax("CONF", 1)
10+
final val PROPERTIES = new ConfigSyntax("PROPERTIES", 2)
11+
12+
private[this] final val _values: Array[ConfigSyntax] =
13+
Array(JSON, CONF, PROPERTIES)
14+
15+
def values: Array[ConfigSyntax] = _values.clone()
16+
17+
def valueOf(name: String): ConfigSyntax =
18+
_values.find(_.name == name).getOrElse {
19+
throw new IllegalArgumentException("No enum const ConfigSyntax." + name)
20+
}
21+
}

tests/pos/trait-java-enum.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
trait T extends java.lang.Enum[T]
2+
3+
enum MyEnum extends T {
4+
case A, B
5+
}

0 commit comments

Comments
 (0)