-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add error message about double definition (#1589) #4064
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello, and thank you for opening this PR! 🎉
All contributors have signed the CLA, thank you! ❤️
Have an awesome day! ☀️
It looks like |
@jendrikw that tests contains double definition, and i suspect the error markers must be updated. I'm new here so I'm not sure why the error count changed—what errors do you get there? You might need to simply add extra
|
@@ -627,7 +627,7 @@ trait Checking { | |||
def explanation = | |||
if (!decl.isRealMethod) "" | |||
else "\n(the definitions have matching type signatures)" | |||
ctx.error(em"$decl is already defined as $other$ofType$explanation", decl.pos) | |||
ctx.error(DoubleDeclaration(decl, other), decl.pos) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe keep using explanation
in the DoubleDeclaration class, or move def explanation
there?
if (!decl.isRealMethod) "" | ||
else "\n(the definitions have matching type signatures)" | ||
ctx.error(em"$decl is already defined as $other$ofType$explanation", decl.pos) | ||
ctx.error(DoubleDeclaration(decl, other), decl.pos) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You must not drop the info on matching type signatures. Either you preserve it or you figure out why it's there by walking git blame
(which leads for instance to #597) and then realize you should preserve it.
Methods can be overloaded, but ths code reject attempts at overloading that define methods with matching signatures.
For instance, for this code from #597 we need to explain users that this sort of overload is not allowed:
trait A
trait B
class Test {
def foo(x: List[A]): Function1[A, A] = ???
def foo(x: List[B]): Function2[B, B, B] = ??? //error
}
I think this code would make a good testcase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your feedback. If I understand correctly, the code given in #597 should not compile. I added a test case in tests/neg/, but the tests failed because it compiles successfully.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uh! I just wanted to say "please don't remove the message", but I should still have tried.
The behavior of decl.matches(other)
probably changed since #597 was filed. I did more experiments and asked around to figure this out.
So, here is the current behavior:
scala> class Test {
def foo(x: List[A]): Function1[A, A] = ???
def foo(x: List[B]): Function1[B, B] = ??? // error
}
3 | def foo(x: List[B]): Function1[B, B] = ??? // error
| ^
| method foo is already defined as method foo: (x: List[A]): A => A
| (the definitions have matching type signatures)
That code fails because, while the overload is "acceptable" (you can tell at call site which one to call, I forget the right word), the two functions erase to the same "signature" (which here means JVM signature, according to @smarter).
The regression I was observing is that here "the definitions have matching type signatures" is important. I'll accept this PR as soon as that message is restored with the same check (I'm not sure why isRealMethod
is tested, please still keep it).
Here or in further PRs, we might want some clearer message there. Scalac gives:
<console>:15: error: double definition:
def foo(x: List[A]): A => A at line 14 and
def foo(x: List[B]): B => B at line 15
have same type after erasure: (x: List)Function1
def foo(x: List[B]): Function1[B, B] = ??? // error
Instead,
class Test {
def foo(x: List[A]): Function1[A, A] = ???
def foo(x: List[B]): Function2[B, B, B] = ??? //error
}
is supported because the overload is acceptable and they erase to different JVM signatures (because of the different return types).
Finally, this code is rejected because this overload isn't acceptable:
scala> class Test {
def foo(x: List[A]): Function1[A, A] = ???
def foo(x: List[A]): Function2[B, B, B] = ??? // error
}
3 | def foo(x: List[A]): Function2[B, B, B] = ??? // error
| ^
| method foo is already defined as method foo: (x: List[A]): A => A
| (the definitions have matching type signatures)
This should arguably give a different error:
scala> class Test {
| def foo(x: List[A]): Function1[A, A] = ???
| def foo(x: List[A]): Function2[B, B, B] = ??? // error
| }
<console>:15: error: method foo is defined twice;
the conflicting method foo was defined at line 14:16
def foo(x: List[A]): Function2[B, B, B] = ??? // error
^
I have tried to improve the error messages by testing for the different cases. Should I put the details in Also, in the error I get from running |
val kind = "Duplicate Symbol" | ||
val msg = { | ||
val details = decl.asTerm.signature.matchDegree(previousSymbol.asTerm.signature) match { | ||
case Signature.NoMatch => "" // matchDegree also returns NoMatch if one of the terms is not a method |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds plausible, but I'm not sure how it relates to realMethod
and why that code is there — not within my "review budget" I fear. I really want to approve this PR but I'm not sure it's not introducing regressions. Finding another reviewer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OTOH, not sure isRealMethod
worked anyway: for abstract class A(val a: Int) { def a: String }
on master I get:
scala> abstract class A(val a: Int) { def a: String }
1 |abstract class A(val a: Int) { def a: String }
| ^
| method a is already defined as value a: Int
| (the definitions have matching type signatures)
I want to check a few details but that looks much better! Not sure on
EDIT: sorry misunderstood, you're adding line numbers (but they don't work), trying to guess why they don't work... |
For extra fun, positions in the REPL appear correct in scala> class Test2 {
def foo(x: List[A]): A => A = ???
def foo(x: List[B]): Function1[B, B] = ???
}
3 | def foo(x: List[B]): Function1[B, B] = ???
| ^
| method foo is already defined as method foo: (x: List[A]): A => A
| (the definitions have matching type signatures) but also
|
Found it, after quite a tour, in /** The line of the position, starting at 0 */ // <--- _documented_
def line: Int = source.offsetToLine(point)
[...]
override def toString =
if (source.exists) s"${source.file}:${line + 1}" I'm afraid you're supposed to add |
And you're right regarding declaration/definition, since turning definitions into declarations doesn't affect the errors — say: abstract class Test3 {
// overload with same argument type, but different return types
def foo(x: List[A]): Function1[A, A]// = ???
def foo(x: List[A]): Function2[B, B, B]// = ??? // error
} I guess we want both in tests. And BTW we'd also want a test for when As a consequence, I'd say Again, you might want to move concerns that feel too hard into separate issues/PRs, or ask for another reviewer (when they're available) — I'm new here, and learning the code with you (reviewing to help others). |
val msg = { | ||
val details = if (decl.isRealMethod && previousSymbol.isRealMethod) { | ||
// compare the signatures when both symbols represent methods | ||
decl.asTerm.signature.matchDegree(previousSymbol.asTerm.signature) match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe you don't need asTerm
val details = if (decl.isRealMethod && previousSymbol.isRealMethod) { | ||
// compare the signatures when both symbols represent methods | ||
decl.asTerm.signature.matchDegree(previousSymbol.asTerm.signature) match { | ||
case Signature.NoMatch => "" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should never happen. I would refactor your code as:
match {
case Signature.FullMatch => ...
case _ /* Signature.ParamMatch */ => ...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know why this is not an enum or ADT? Was is because of performance reasons? Using Ints is very unidiomatic scala IMO.
decl.asTerm.signature.matchDegree(previousSymbol.asTerm.signature) match { | ||
case Signature.NoMatch => "" | ||
case Signature.ParamMatch => "\nOverloads with equal parameter types but different return types are not allowed." | ||
case Signature.FullMatch => "\nThe definitions have the same signature after erasure." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The definitions have the same matching type signatures after erasure.
// compare the signatures when both symbols represent methods | ||
decl.asTerm.signature.matchDegree(previousSymbol.asTerm.signature) match { | ||
case Signature.NoMatch => "" | ||
case Signature.ParamMatch => "\nOverloads with equal parameter types but different return types are not allowed." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overloads with matching parameter types are not allowed
case Signature.FullMatch => "\nThe definitions have the same signature after erasure." | ||
} | ||
} else "" | ||
hl"${decl.showLocated} is already defined as ${previousSymbol.showDcl} in line ${previousSymbol.pos.line + 1}." + details |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in at line
tests/neg/singletonOrs.scala
Outdated
@@ -1,6 +1,6 @@ | |||
object Test { | |||
def foo: 1 | 2 = 1 // error // error | |||
def bar: 3 | 4 = foo // error // error | |||
def foo: 1 | 2 = 1 // error // error | |||
def bar: 1 = foo | |||
def foo: 1 | 2 = 1 // error // error // error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@smarter This is weird, the change seems unrelated. You might want to look into this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure about the internals, but I'm guessing that the error about singleton types in unions are from the typer phase and the compiler exists afterwards. But that wouldn't explain the different behaviour in this branch.
Manually compiling tests/neg/singletonOrs.scala
gives
2 | def foo: 1 | 2 = 1 // error // error
| ^
| Singleton type Int(1) is not allowed in a union type
-- Error: tests/neg/singletonOrs.scala:2:16 ------------------------------------
2 | def foo: 1 | 2 = 1 // error // error
| ^
| Singleton type Int(2) is not allowed in a union type
-- Error: tests/neg/singletonOrs.scala:3:12 ------------------------------------
3 | def bar: 3 | 4 = foo // error // error
| ^
| Singleton type Int(3) is not allowed in a union type
-- Error: tests/neg/singletonOrs.scala:3:16 ------------------------------------
3 | def bar: 3 | 4 = foo // error // error
| ^
| Singleton type Int(4) is not allowed in a union type
-- Error: tests/neg/singletonOrs.scala:4:12 ------------------------------------
4 | def foo: 1 | 2 = 1 // error // error // error
| ^
| Singleton type Int(1) is not allowed in a union type
-- Error: tests/neg/singletonOrs.scala:4:16 ------------------------------------
4 | def foo: 1 | 2 = 1 // error // error // error
| ^
| Singleton type Int(2) is not allowed in a union type
-- [E119] Duplicate Symbol Error: tests/neg/singletonOrs.scala:4:7 -------------
4 | def foo: 1 | 2 = 1 // error // error // error
| ^
|method foo in object Test is already defined as def foo: =>
| <error Singleton type Int(1) is not allowed in a union type> |
| <error Singleton type Int(2) is not allowed in a union type> at line 2.
|The definitions have matching type signatures after erasure.
-- [E119] Duplicate Symbol Error: tests/neg/singletonOrs.scala:5:7 -------------
5 | def bar: 1 = foo // error
| ^
|method bar in object Test is already defined as def bar: =>
| <error Singleton type Int(3) is not allowed in a union type> |
| <error Singleton type Int(4) is not allowed in a union type> at line 3.
|Overloads with matching parameter types are not allowed.
I think the last two error messages are very confusing. The previous error is displayed when previousDecl.showDcl
is called. That seemed like a nice way to show the conflicting declaration. Is there an alternative?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Until you get more competent answers) If showDcl
is the proper API (as I'm guessing), my ignorant vote is to stick to showDcl
and maybe open an issue to improve its output.
On the error count, I suspect it's some side effect of how errors are combined/filtered—but not sure.
tests/neg/singletonOrs.scala
Outdated
def foo: 1 | 2 = 1 // error // error | ||
def bar: 1 = foo | ||
def foo: 1 | 2 = 1 // error // error // error | ||
def bar: 1 = foo // error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I think a better solution is to rename the last two methods. This test is not meant to test duplicate definition
Waiting for @allanrenucci before merging. |
The old error message said "definition", but I think I should be called declaration. Please clearify this. I will update the code if needed.