Skip to content

Commit 6310ec2

Browse files
committed
Enforce matching targetNames when overriding
1 parent 039f42f commit 6310ec2

File tree

6 files changed

+123
-73
lines changed

6 files changed

+123
-73
lines changed

compiler/src/dotty/tools/dotc/config/Printers.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ object Printers {
3737
val pickling = noPrinter
3838
val quotePickling = noPrinter
3939
val plugins = noPrinter
40+
val refcheck = noPrinter
4041
val simplify = noPrinter
4142
val staging = noPrinter
4243
val subtyping = noPrinter

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

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -986,35 +986,36 @@ object Denotations {
986986

987987
def matches(other: SingleDenotation)(using Context): Boolean =
988988
symbol.hasTargetName(other.symbol.targetName)
989-
&& {
990-
val d = signature.matchDegree(other.signature)
991-
d match
992-
case FullMatch =>
993-
true
994-
case MethodNotAMethodMatch =>
995-
!ctx.erasedTypes && {
996-
val isJava = symbol.is(JavaDefined)
997-
val otherIsJava = other.symbol.is(JavaDefined)
998-
// A Scala zero-parameter method and a Scala non-method always match.
999-
if !isJava && !otherIsJava then
1000-
true
1001-
// Java allows defining both a field and a zero-parameter method with the same name,
1002-
// so they must not match.
1003-
else if isJava && otherIsJava then
1004-
false
1005-
// A Java field never matches a Scala method.
1006-
else if isJava then
1007-
symbol.is(Method)
1008-
else // otherIsJava
1009-
other.symbol.is(Method)
1010-
}
1011-
case ParamMatch =>
1012-
// The signatures do not tell us enough to be sure about matching
1013-
!ctx.erasedTypes && info.matches(other.info)
1014-
case noMatch =>
1015-
false
1016-
}
1017-
end matches
989+
&& matchesLoosely(other)
990+
991+
/** matches without a target name check */
992+
def matchesLoosely(other: SingleDenotation)(using Context): Boolean =
993+
val d = signature.matchDegree(other.signature)
994+
d match
995+
case FullMatch =>
996+
true
997+
case MethodNotAMethodMatch =>
998+
!ctx.erasedTypes && {
999+
val isJava = symbol.is(JavaDefined)
1000+
val otherIsJava = other.symbol.is(JavaDefined)
1001+
// A Scala zero-parameter method and a Scala non-method always match.
1002+
if !isJava && !otherIsJava then
1003+
true
1004+
// Java allows defining both a field and a zero-parameter method with the same name,
1005+
// so they must not match.
1006+
else if isJava && otherIsJava then
1007+
false
1008+
// A Java field never matches a Scala method.
1009+
else if isJava then
1010+
symbol.is(Method)
1011+
else // otherIsJava
1012+
other.symbol.is(Method)
1013+
}
1014+
case ParamMatch =>
1015+
// The signatures do not tell us enough to be sure about matching
1016+
!ctx.erasedTypes && info.matches(other.info)
1017+
case noMatch =>
1018+
false
10181019

10191020
def mapInherited(ownDenots: PreDenotation, prevDenots: PreDenotation, pre: Type)(using Context): SingleDenotation =
10201021
if hasUniqueSym && prevDenots.containsSym(symbol) then NoDenotation

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

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import config.{ScalaVersion, NoScalaVersion}
1818
import Decorators._
1919
import typer.ErrorReporting._
2020
import config.Feature.{warnOnMigration, migrateTo3}
21+
import config.Printers.refcheck
2122
import reporting._
2223
import scala.util.matching.Regex._
2324
import Constants.Constant
@@ -236,6 +237,26 @@ object RefChecks {
236237
i"${if (showLocation) sym1.showLocated else sym1}$infoStr"
237238
}
238239

240+
def compatibleTypes(member: Symbol, memberTp: Type, other: Symbol, otherTp: Type, fallBack: => Boolean = false): Boolean =
241+
try
242+
if (member.isType) // intersection of bounds to refined types must be nonempty
243+
memberTp.bounds.hi.hasSameKindAs(otherTp.bounds.hi) &&
244+
((memberTp frozen_<:< otherTp) ||
245+
!member.owner.derivesFrom(other.owner) && {
246+
// if member and other come from independent classes or traits, their
247+
// bounds must have non-empty-intersection
248+
val jointBounds = (memberTp.bounds & otherTp.bounds).bounds
249+
jointBounds.lo frozen_<:< jointBounds.hi
250+
})
251+
else
252+
member.name.is(DefaultGetterName) || // default getters are not checked for compatibility
253+
memberTp.overrides(otherTp,
254+
member.matchNullaryLoosely || other.matchNullaryLoosely || fallBack)
255+
catch case ex: MissingType =>
256+
// can happen when called with upwardsSelf as qualifier of memberTp and otherTp,
257+
// because in that case we might access types that are not members of the qualifier.
258+
false
259+
239260
/* Check that all conditions for overriding `other` by `member`
240261
* of class `clazz` are met.
241262
*/
@@ -245,7 +266,7 @@ object RefChecks {
245266
else self.memberInfo(member)
246267
def otherTp(self: Type) = self.memberInfo(other)
247268

248-
report.debuglog("Checking validity of %s overriding %s".format(member.showLocated, other.showLocated))
269+
refcheck.println(i"check override ${infoString(member)} overriding ${infoString(other)}")
249270

250271
def noErrorType = !memberTp(self).isErroneous && !otherTp(self).isErroneous
251272

@@ -265,6 +286,12 @@ object RefChecks {
265286
infoStringWithLocation(other), infoString(member), msg, addendum)
266287
}
267288

289+
def compatTypes(memberTp: Type, otherTp: Type): Boolean =
290+
compatibleTypes(member, memberTp, other, otherTp,
291+
fallBack = warnOnMigration(
292+
overrideErrorMsg("no longer has compatible type"),
293+
(if (member.owner == clazz) member else clazz).srcPos))
294+
268295
def emitOverrideError(fullmsg: String) =
269296
if (!(hasErrors && member.is(Synthetic) && member.is(Module))) {
270297
// suppress errors relating toi synthetic companion objects if other override
@@ -291,29 +318,14 @@ object RefChecks {
291318
(if (otherAccess == "") "public" else "at least " + otherAccess))
292319
}
293320

294-
def compatibleTypes(memberTp: Type, otherTp: Type): Boolean =
295-
try
296-
if (member.isType) // intersection of bounds to refined types must be nonempty
297-
memberTp.bounds.hi.hasSameKindAs(otherTp.bounds.hi) &&
298-
((memberTp frozen_<:< otherTp) ||
299-
!member.owner.derivesFrom(other.owner) && {
300-
// if member and other come from independent classes or traits, their
301-
// bounds must have non-empty-intersection
302-
val jointBounds = (memberTp.bounds & otherTp.bounds).bounds
303-
jointBounds.lo frozen_<:< jointBounds.hi
304-
})
305-
else
306-
member.name.is(DefaultGetterName) || // default getters are not checked for compatibility
307-
memberTp.overrides(otherTp,
308-
member.matchNullaryLoosely || other.matchNullaryLoosely ||
309-
warnOnMigration(overrideErrorMsg("no longer has compatible type"),
310-
(if (member.owner == clazz) member else clazz).srcPos))
311-
catch {
312-
case ex: MissingType =>
313-
// can happen when called with upwardsSelf as qualifier of memberTp and otherTp,
314-
// because in that case we might access types that are not members of the qualifier.
315-
false
316-
}
321+
def overrideTargetNameError() =
322+
val otherTargetName = i"@targetName(${other.targetName})"
323+
if member.hasTargetName(member.name) then
324+
overrideError(i"misses a target name annotation $otherTargetName")
325+
else if other.hasTargetName(other.name) then
326+
overrideError(i"should not have a @targetName annotation since the overridden member hasn't one either")
327+
else
328+
overrideError(i"has a different target name annotation; it should be $otherTargetName")
317329

318330
//Console.println(infoString(member) + " overrides " + infoString(other) + " in " + clazz);//DEBUG
319331

@@ -359,7 +371,9 @@ object RefChecks {
359371
&& (ob.isContainedIn(mb) || other.isAllOf(JavaProtected))
360372
// m relaxes o's access boundary,
361373
// or o is Java defined and protected (see #3946)
362-
if (!isOverrideAccessOK)
374+
if !member.hasTargetName(other.targetName) then
375+
overrideTargetNameError()
376+
else if (!isOverrideAccessOK)
363377
overrideAccessError()
364378
else if (other.isClass)
365379
// direct overrides were already checked on completion (see Checking.chckWellFormed)
@@ -428,8 +442,8 @@ object RefChecks {
428442
overrideError("is not inline, cannot implement an inline method")
429443
else if (other.isScala2Macro && !member.isScala2Macro) // (1.11)
430444
overrideError("cannot be used here - only Scala-2 macros can override Scala-2 macros")
431-
else if (!compatibleTypes(memberTp(self), otherTp(self)) &&
432-
!compatibleTypes(memberTp(upwardsSelf), otherTp(upwardsSelf)))
445+
else if (!compatTypes(memberTp(self), otherTp(self)) &&
446+
!compatTypes(memberTp(upwardsSelf), otherTp(upwardsSelf)))
433447
overrideError("has incompatible type" + err.whyNoMatchStr(memberTp(self), otherTp(self)))
434448
else if (member.targetName != other.targetName)
435449
if (other.targetName != other.name)
@@ -451,7 +465,27 @@ object RefChecks {
451465
}*/
452466
}
453467

454-
val opc = new OverridingPairs.Cursor(clazz)
468+
val opc = new OverridingPairs.Cursor(clazz):
469+
470+
/** We declare a match if either we have a full match including matching names
471+
* or we have a loose match with different target name but the types are the same.
472+
* This leaves two possible sorts of discrepancies to be reported as errors
473+
* in `checkOveride`:
474+
*
475+
* - matching names, target names, and signatures but different types
476+
* - matching names and types, but different target names
477+
*/
478+
override def matches(sym1: Symbol, sym2: Symbol): Boolean =
479+
sym1.isType
480+
|| {
481+
val sd1 = sym1.asSeenFrom(clazz.thisType)
482+
val sd2 = sym2.asSeenFrom(clazz.thisType)
483+
sd1.matchesLoosely(sd2)
484+
&& (sym1.hasTargetName(sym2.targetName)
485+
|| compatibleTypes(sym1, sd1.info, sym2, sd2.info))
486+
}
487+
end opc
488+
455489
while opc.hasNext do
456490
checkOverride(opc.overriding, opc.overridden)
457491
opc.next()

tests/neg/targetName-override.check

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
-- Error: tests/neg/targetName-override.scala:16:35 --------------------------------------------------------------------
2+
16 | @targetName("foo1") override def foo() = 1 // error: should not have a target name
3+
| ^
4+
|error overriding method foo in class Alpha of type (): Int;
5+
| method foo of type (): Int should not have a @targetName annotation since the overridden member hasn't one either
6+
-- Error: tests/neg/targetName-override.scala:18:25 --------------------------------------------------------------------
7+
18 | @targetName("baz") def foo(x: String): String = x ++ x // error: has a different target name annotation
8+
| ^
9+
| error overriding method foo in class Alpha of type (x: String): String;
10+
| method foo of type (x: String): String has a different target name annotation; it should be @targetName(foo1)
11+
-- Error: tests/neg/targetName-override.scala:20:15 --------------------------------------------------------------------
12+
20 | override def ++ (xs: Alpha[String]): Alpha[String] = this // error: misses a targetname annotation
13+
| ^
14+
| error overriding method ++ in class Alpha of type (xs: Alpha[String]): Alpha[String];
15+
| method ++ of type (xs: Alpha[String]): Alpha[String] misses a target name annotation @targetName(append)
116
-- Error: tests/neg/targetName-override.scala:14:6 ---------------------------------------------------------------------
217
14 |class Beta extends Alpha[String] { // error: needs to be abstract
318
| ^
419
|class Beta needs to be abstract, since there is a deferred declaration of method foo in class Alpha of type (x: String): String which is not implemented in a subclass
5-
-- [E038] Declaration Error: tests/neg/targetName-override.scala:16:35 -------------------------------------------------
6-
16 | @targetName("foo1") override def foo() = 1 // error: different signature than overridden
7-
| ^
8-
| method foo has a different target name than the overridden declaration
9-
10-
longer explanation available when compiling with `-explain`
11-
-- [E038] Declaration Error: tests/neg/targetName-override.scala:20:15 -------------------------------------------------
12-
20 | override def ++ (xs: Alpha[String]): Alpha[String] = this // error: different signature than overidden
13-
| ^
14-
| method ++ has a different target name than the overridden declaration
15-
16-
longer explanation available when compiling with `-explain`

tests/neg/targetName-override.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ abstract class Alpha[T] {
55

66
def foo() = 1
77

8-
@targetName("bar") def foo(x: T): T
8+
@targetName("foo1") def foo(x: T): T
99

1010
@targetName("append") def ++ (xs: Alpha[T]): Alpha[T] = this
1111

1212
}
1313

1414
class Beta extends Alpha[String] { // error: needs to be abstract
1515

16-
@targetName("foo1") override def foo() = 1 // error: different signature than overridden
16+
@targetName("foo1") override def foo() = 1 // error: should not have a target name
1717

18-
@targetName("baz") def foo(x: String): String = x ++ x // OK,
18+
@targetName("baz") def foo(x: String): String = x ++ x // error: has a different target name annotation
1919

20-
override def ++ (xs: Alpha[String]): Alpha[String] = this // error: different signature than overidden
20+
override def ++ (xs: Alpha[String]): Alpha[String] = this // error: misses a targetname annotation
2121

2222
}

tests/pos/targetName-override.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import scala.annotation.targetName
2+
3+
class A:
4+
@targetName("ff") def f(x: => Int): Int = x
5+
def f(x: => String): Int = x.length
6+
7+
8+
class B extends A:
9+
@targetName("ff") override def f(x: => Int): Int = x // OK
10+
override def f(x: => String): Int = x.length // OK
11+

0 commit comments

Comments
 (0)