-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Implement -Wvalue-discard warning #15975
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
Changes from all commits
9dc1404
fae048c
ef34c69
5e05468
cb99f7c
702c485
83a01b8
592dccf
8185517
973829b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -110,6 +110,24 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] => | |
case _ => 0 | ||
} | ||
|
||
/** The type arguments of a possibly curried call */ | ||
def typeArgss(tree: Tree): List[List[Tree]] = | ||
@tailrec | ||
def loop(tree: Tree, argss: List[List[Tree]]): List[List[Tree]] = tree match | ||
case TypeApply(fn, args) => loop(fn, args :: argss) | ||
case Apply(fn, args) => loop(fn, argss) | ||
case _ => argss | ||
loop(tree, Nil) | ||
|
||
/** The term arguments of a possibly curried call */ | ||
def termArgss(tree: Tree): List[List[Tree]] = | ||
@tailrec | ||
def loop(tree: Tree, argss: List[List[Tree]]): List[List[Tree]] = tree match | ||
case Apply(fn, args) => loop(fn, args :: argss) | ||
case TypeApply(fn, args) => loop(fn, argss) | ||
case _ => argss | ||
loop(tree, Nil) | ||
|
||
/** All term arguments of an application in a single flattened list */ | ||
def allArguments(tree: Tree): List[Tree] = unsplice(tree) match { | ||
case Apply(fn, args) => allArguments(fn) ::: args | ||
|
@@ -308,6 +326,132 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] => | |
case Block(_, expr) => forallResults(expr, p) | ||
case _ => p(tree) | ||
} | ||
|
||
/** Applications in Scala can have one of the following shapes: | ||
* | ||
* 1) naked core: Ident(_) or Select(_, _) or basically anything else | ||
* 2) naked core with targs: TypeApply(core, targs) or AppliedTypeTree(core, targs) | ||
* 3) apply or several applies wrapping a core: Apply(core, _), or Apply(Apply(core, _), _), etc | ||
* | ||
* This class provides different ways to decompose applications and simplifies their analysis. | ||
* | ||
* ***Examples*** | ||
* (TypeApply in the examples can be replaced with AppliedTypeTree) | ||
* | ||
* Ident(foo): | ||
* * callee = Ident(foo) | ||
* * core = Ident(foo) | ||
* * targs = Nil | ||
* * argss = Nil | ||
* | ||
* TypeApply(foo, List(targ1, targ2...)) | ||
* * callee = TypeApply(foo, List(targ1, targ2...)) | ||
* * core = foo | ||
* * targs = List(targ1, targ2...) | ||
* * argss = Nil | ||
* | ||
* Apply(foo, List(arg1, arg2...)) | ||
* * callee = foo | ||
* * core = foo | ||
* * targs = Nil | ||
* * argss = List(List(arg1, arg2...)) | ||
* | ||
* Apply(Apply(foo, List(arg21, arg22, ...)), List(arg11, arg12...)) | ||
* * callee = foo | ||
* * core = foo | ||
* * targs = Nil | ||
* * argss = List(List(arg21, arg22, ...), List(arg11, arg12, ...)) | ||
* | ||
* Apply(Apply(TypeApply(foo, List(targs1, targs2, ...)), List(arg21, arg22, ...)), List(arg11, arg12...)) | ||
* * callee = TypeApply(foo, List(targs1, targs2, ...)) | ||
* * core = foo | ||
* * targs = Nil | ||
* * argss = List(List(arg21, arg22, ...), List(arg11, arg12, ...)) | ||
*/ | ||
final class Applied(val tree: Tree) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we possibly reuse some of the existing utility methods e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the pointer. When I was copying swathes of code from Scala 2, I didn't noticed those almost-identical methods already existed. I've refactored |
||
/** The tree stripped of the possibly nested applications. | ||
* The original tree if it's not an application. | ||
*/ | ||
def callee: Tree = stripApply(tree) | ||
|
||
/** The `callee` unwrapped from type applications. | ||
* The original `callee` if it's not a type application. | ||
*/ | ||
def core: Tree = callee match { | ||
case TypeApply(fn, _) => fn | ||
case AppliedTypeTree(fn, _) => fn | ||
case tree => tree | ||
} | ||
|
||
/** The type arguments of the `callee`. | ||
* `Nil` if the `callee` is not a type application. | ||
*/ | ||
def targs: List[Tree] = callee match { | ||
case TypeApply(_, args) => args | ||
case AppliedTypeTree(_, args) => args | ||
case _ => Nil | ||
} | ||
|
||
/** (Possibly multiple lists of) value arguments of an application. | ||
* `Nil` if the `callee` is not an application. | ||
*/ | ||
def argss: List[List[Tree]] = termArgss(tree) | ||
} | ||
|
||
/** Destructures applications into important subparts described in `Applied` class, | ||
* namely into: core, targs and argss (in the specified order). | ||
* | ||
* Trees which are not applications are also accepted. Their callee and core will | ||
* be equal to the input, while targs and argss will be Nil. | ||
* | ||
* The provided extractors don't expose all the API of the `Applied` class. | ||
* For advanced use, call `dissectApplied` explicitly and use its methods instead of pattern matching. | ||
*/ | ||
object Applied { | ||
def apply(tree: Tree): Applied = new Applied(tree) | ||
|
||
def unapply(applied: Applied): Some[(Tree, List[Tree], List[List[Tree]])] = | ||
Some((applied.core, applied.targs, applied.argss)) | ||
|
||
def unapply(tree: Tree): Some[(Tree, List[Tree], List[List[Tree]])] = | ||
unapply(new Applied(tree)) | ||
} | ||
|
||
/** Is tree an application with result `this.type`? | ||
* Accept `b.addOne(x)` and also `xs(i) += x` | ||
* where the op is an assignment operator. | ||
*/ | ||
def isThisTypeResult(tree: Tree)(using Context): Boolean = tree match { | ||
case Applied(fun @ Select(receiver, op), _, argss) => | ||
tree.tpe match { | ||
case ThisType(tref) => | ||
tref.symbol == receiver.symbol | ||
case tref: TermRef => | ||
tref.symbol == receiver.symbol || argss.exists(_.exists(tref.symbol == _.symbol)) | ||
case _ => | ||
def checkSingle(sym: Symbol): Boolean = | ||
(sym == receiver.symbol) || { | ||
receiver match { | ||
case Apply(_, _) => op.isOpAssignmentName // xs(i) += x | ||
case _ => receiver.symbol != NoSymbol && | ||
(receiver.symbol.isGetter || receiver.symbol.isField) // xs.addOne(x) for var xs | ||
} | ||
} | ||
@tailrec def loop(mt: Type): Boolean = mt match { | ||
case m: MethodType => | ||
m.resType match { | ||
case ThisType(tref) => checkSingle(tref.symbol) | ||
case tref: TermRef => checkSingle(tref.symbol) | ||
case restpe => loop(restpe) | ||
} | ||
case PolyType(_, restpe) => loop(restpe) | ||
case _ => false | ||
} | ||
fun.symbol != NoSymbol && loop(fun.symbol.info) | ||
} | ||
case _ => | ||
tree.tpe.isInstanceOf[ThisType] | ||
} | ||
} | ||
|
||
trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] => | ||
|
@@ -683,24 +827,6 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => | |
} | ||
} | ||
|
||
/** The type arguments of a possibly curried call */ | ||
def typeArgss(tree: Tree): List[List[Tree]] = | ||
@tailrec | ||
def loop(tree: Tree, argss: List[List[Tree]]): List[List[Tree]] = tree match | ||
case TypeApply(fn, args) => loop(fn, args :: argss) | ||
case Apply(fn, args) => loop(fn, argss) | ||
case _ => argss | ||
loop(tree, Nil) | ||
|
||
/** The term arguments of a possibly curried call */ | ||
def termArgss(tree: Tree): List[List[Tree]] = | ||
@tailrec | ||
def loop(tree: Tree, argss: List[List[Tree]]): List[List[Tree]] = tree match | ||
case Apply(fn, args) => loop(fn, args :: argss) | ||
case TypeApply(fn, args) => loop(fn, argss) | ||
case _ => argss | ||
loop(tree, Nil) | ||
|
||
/** The type and term arguments of a possibly curried call, in the order they are given */ | ||
def allArgss(tree: Tree): List[List[Tree]] = | ||
@tailrec | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
-- [E175] Potential Issue Error: tests/neg/warn-value-discard.scala:15:35 ---------------------------------------------- | ||
15 | firstThing().map(_ => secondThing()) // error | ||
| ^^^^^^^^^^^^^ | ||
| discarded non-Unit value of type Either[Failed, Unit] | ||
-- [E175] Potential Issue Error: tests/neg/warn-value-discard.scala:18:35 ---------------------------------------------- | ||
18 | firstThing().map(_ => secondThing()) // error | ||
| ^^^^^^^^^^^^^ | ||
| discarded non-Unit value of type Either[Failed, Unit] | ||
-- [E175] Potential Issue Error: tests/neg/warn-value-discard.scala:27:36 ---------------------------------------------- | ||
27 | mutable.Set.empty[String].remove("") // error | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| discarded non-Unit value of type Boolean | ||
-- [E175] Potential Issue Error: tests/neg/warn-value-discard.scala:39:41 ---------------------------------------------- | ||
39 | mutable.Set.empty[String].subtractOne("") // error | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| discarded non-Unit value of type scala.collection.mutable.Set[String] | ||
-- [E175] Potential Issue Error: tests/neg/warn-value-discard.scala:59:4 ----------------------------------------------- | ||
59 | mutable.Set.empty[String] += "" // error | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| discarded non-Unit value of type scala.collection.mutable.Set[String] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// scalac: -Wvalue-discard -Werror | ||
|
||
import scala.util.{Either, Right, Left} | ||
import scala.collection.mutable | ||
|
||
case class Failed(msg: String) | ||
|
||
def firstThing(): Either[Failed, Unit] = | ||
Right(()) | ||
|
||
def secondThing(): Either[Failed, Unit] = | ||
Left(Failed("whoops you should have flatMapped me")) | ||
|
||
def singleExpr(): Either[Failed, Unit] = | ||
firstThing().map(_ => secondThing()) // error | ||
|
||
def block(): Either[Failed, Unit] = { | ||
firstThing().map(_ => secondThing()) // error | ||
} | ||
|
||
class ValueDiscardTest: | ||
val field = mutable.Set.empty[String] | ||
|
||
def remove(): Unit = | ||
// Set#remove returns a Boolean, not this.type | ||
// --> Warning | ||
mutable.Set.empty[String].remove("") // error | ||
|
||
// TODO IMHO we don't need to support this, | ||
// as it's just as easy to add a @nowarn annotation as a Unit ascription | ||
//def removeAscribed(): Unit = { | ||
// mutable.Set.empty[String].remove(""): Unit // nowarn | ||
//} | ||
|
||
def subtract(): Unit = | ||
// - Set#subtractOne returns this.type | ||
// - receiver is not a field or a local variable (not quite sure what you'd call it) | ||
// --> Warning | ||
mutable.Set.empty[String].subtractOne("") // error | ||
|
||
def mutateLocalVariable(): Unit = { | ||
// - Set#subtractOne returns this.type | ||
// - receiver is a local variable | ||
// --> No warning | ||
val s: mutable.Set[String] = mutable.Set.empty[String] | ||
s.subtractOne("") | ||
} | ||
|
||
def mutateField(): Unit = | ||
// - Set#subtractOne returns this.type | ||
// - receiver is a local variable | ||
// --> No warning | ||
field.subtractOne("") | ||
|
||
def assignmentOperator(): Unit = | ||
// - += returns this.type | ||
// - receiver is not a field or a local variable | ||
// --> Warning | ||
mutable.Set.empty[String] += "" // error | ||
|
||
def assignmentOperatorLocalVariable(): Unit = | ||
// - += returns this.type | ||
// - receiver is a local variable | ||
// --> No warning | ||
val s: mutable.Set[String] = mutable.Set.empty[String] | ||
s += "" |
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 Scala 3 a
TypeApply
can also wrap anApply
because of how extension methods are desugared and soon because of #14019There 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.
(And an AppliedTypeTree can wrap another AppliedTypeTree, e.g.
type Foo[X] = [Y] =>> Y; val x: Foo[Int][String] = ""
)