Skip to content

Commit 667d904

Browse files
Implement -Wvalue-discard warning (#15975)
Warn when an expression `e` with non-Unit type is adapted by embedding it into a block `{ e; () }` because the expected type is Unit. In Scala 2.13 ([relevant code in Typers.scala](https://github.com/scala/scala/blob/d578a02ea6b41b662072759c82c19f9309a15176/src/compiler/scala/tools/nsc/typechecker/Typers.scala#L1116)) there are two special cases: 1. The warning can be disabled by explicitly annotating an expression with `: Unit`. * In this PR I've elected not to implement this, as IMO it's not necessary now we have `@nowarn`. It's just as easy to annotate with `@nowarn` as with `: Unit`. 2. No warning is raised if the expression is an application of a function that returns `this.type`, because this is a very common operation when using mutable collections. * The heuristic is actually slightly more subtle than that. See the `warn-value-discard.scala` test for some examples. * I have implemented this special case by copying the relevant code from the `2.13.x` branch in scala/scala, so the behaviour should be equivalent to Scala 2.13.
2 parents 37d9640 + 973829b commit 667d904

File tree

7 files changed

+240
-18
lines changed

7 files changed

+240
-18
lines changed

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

Lines changed: 144 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,24 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] =>
110110
case _ => 0
111111
}
112112

113+
/** The type arguments of a possibly curried call */
114+
def typeArgss(tree: Tree): List[List[Tree]] =
115+
@tailrec
116+
def loop(tree: Tree, argss: List[List[Tree]]): List[List[Tree]] = tree match
117+
case TypeApply(fn, args) => loop(fn, args :: argss)
118+
case Apply(fn, args) => loop(fn, argss)
119+
case _ => argss
120+
loop(tree, Nil)
121+
122+
/** The term arguments of a possibly curried call */
123+
def termArgss(tree: Tree): List[List[Tree]] =
124+
@tailrec
125+
def loop(tree: Tree, argss: List[List[Tree]]): List[List[Tree]] = tree match
126+
case Apply(fn, args) => loop(fn, args :: argss)
127+
case TypeApply(fn, args) => loop(fn, argss)
128+
case _ => argss
129+
loop(tree, Nil)
130+
113131
/** All term arguments of an application in a single flattened list */
114132
def allArguments(tree: Tree): List[Tree] = unsplice(tree) match {
115133
case Apply(fn, args) => allArguments(fn) ::: args
@@ -308,6 +326,132 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] =>
308326
case Block(_, expr) => forallResults(expr, p)
309327
case _ => p(tree)
310328
}
329+
330+
/** Applications in Scala can have one of the following shapes:
331+
*
332+
* 1) naked core: Ident(_) or Select(_, _) or basically anything else
333+
* 2) naked core with targs: TypeApply(core, targs) or AppliedTypeTree(core, targs)
334+
* 3) apply or several applies wrapping a core: Apply(core, _), or Apply(Apply(core, _), _), etc
335+
*
336+
* This class provides different ways to decompose applications and simplifies their analysis.
337+
*
338+
* ***Examples***
339+
* (TypeApply in the examples can be replaced with AppliedTypeTree)
340+
*
341+
* Ident(foo):
342+
* * callee = Ident(foo)
343+
* * core = Ident(foo)
344+
* * targs = Nil
345+
* * argss = Nil
346+
*
347+
* TypeApply(foo, List(targ1, targ2...))
348+
* * callee = TypeApply(foo, List(targ1, targ2...))
349+
* * core = foo
350+
* * targs = List(targ1, targ2...)
351+
* * argss = Nil
352+
*
353+
* Apply(foo, List(arg1, arg2...))
354+
* * callee = foo
355+
* * core = foo
356+
* * targs = Nil
357+
* * argss = List(List(arg1, arg2...))
358+
*
359+
* Apply(Apply(foo, List(arg21, arg22, ...)), List(arg11, arg12...))
360+
* * callee = foo
361+
* * core = foo
362+
* * targs = Nil
363+
* * argss = List(List(arg21, arg22, ...), List(arg11, arg12, ...))
364+
*
365+
* Apply(Apply(TypeApply(foo, List(targs1, targs2, ...)), List(arg21, arg22, ...)), List(arg11, arg12...))
366+
* * callee = TypeApply(foo, List(targs1, targs2, ...))
367+
* * core = foo
368+
* * targs = Nil
369+
* * argss = List(List(arg21, arg22, ...), List(arg11, arg12, ...))
370+
*/
371+
final class Applied(val tree: Tree) {
372+
/** The tree stripped of the possibly nested applications.
373+
* The original tree if it's not an application.
374+
*/
375+
def callee: Tree = stripApply(tree)
376+
377+
/** The `callee` unwrapped from type applications.
378+
* The original `callee` if it's not a type application.
379+
*/
380+
def core: Tree = callee match {
381+
case TypeApply(fn, _) => fn
382+
case AppliedTypeTree(fn, _) => fn
383+
case tree => tree
384+
}
385+
386+
/** The type arguments of the `callee`.
387+
* `Nil` if the `callee` is not a type application.
388+
*/
389+
def targs: List[Tree] = callee match {
390+
case TypeApply(_, args) => args
391+
case AppliedTypeTree(_, args) => args
392+
case _ => Nil
393+
}
394+
395+
/** (Possibly multiple lists of) value arguments of an application.
396+
* `Nil` if the `callee` is not an application.
397+
*/
398+
def argss: List[List[Tree]] = termArgss(tree)
399+
}
400+
401+
/** Destructures applications into important subparts described in `Applied` class,
402+
* namely into: core, targs and argss (in the specified order).
403+
*
404+
* Trees which are not applications are also accepted. Their callee and core will
405+
* be equal to the input, while targs and argss will be Nil.
406+
*
407+
* The provided extractors don't expose all the API of the `Applied` class.
408+
* For advanced use, call `dissectApplied` explicitly and use its methods instead of pattern matching.
409+
*/
410+
object Applied {
411+
def apply(tree: Tree): Applied = new Applied(tree)
412+
413+
def unapply(applied: Applied): Some[(Tree, List[Tree], List[List[Tree]])] =
414+
Some((applied.core, applied.targs, applied.argss))
415+
416+
def unapply(tree: Tree): Some[(Tree, List[Tree], List[List[Tree]])] =
417+
unapply(new Applied(tree))
418+
}
419+
420+
/** Is tree an application with result `this.type`?
421+
* Accept `b.addOne(x)` and also `xs(i) += x`
422+
* where the op is an assignment operator.
423+
*/
424+
def isThisTypeResult(tree: Tree)(using Context): Boolean = tree match {
425+
case Applied(fun @ Select(receiver, op), _, argss) =>
426+
tree.tpe match {
427+
case ThisType(tref) =>
428+
tref.symbol == receiver.symbol
429+
case tref: TermRef =>
430+
tref.symbol == receiver.symbol || argss.exists(_.exists(tref.symbol == _.symbol))
431+
case _ =>
432+
def checkSingle(sym: Symbol): Boolean =
433+
(sym == receiver.symbol) || {
434+
receiver match {
435+
case Apply(_, _) => op.isOpAssignmentName // xs(i) += x
436+
case _ => receiver.symbol != NoSymbol &&
437+
(receiver.symbol.isGetter || receiver.symbol.isField) // xs.addOne(x) for var xs
438+
}
439+
}
440+
@tailrec def loop(mt: Type): Boolean = mt match {
441+
case m: MethodType =>
442+
m.resType match {
443+
case ThisType(tref) => checkSingle(tref.symbol)
444+
case tref: TermRef => checkSingle(tref.symbol)
445+
case restpe => loop(restpe)
446+
}
447+
case PolyType(_, restpe) => loop(restpe)
448+
case _ => false
449+
}
450+
fun.symbol != NoSymbol && loop(fun.symbol.info)
451+
}
452+
case _ =>
453+
tree.tpe.isInstanceOf[ThisType]
454+
}
311455
}
312456

313457
trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] =>
@@ -683,24 +827,6 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
683827
}
684828
}
685829

686-
/** The type arguments of a possibly curried call */
687-
def typeArgss(tree: Tree): List[List[Tree]] =
688-
@tailrec
689-
def loop(tree: Tree, argss: List[List[Tree]]): List[List[Tree]] = tree match
690-
case TypeApply(fn, args) => loop(fn, args :: argss)
691-
case Apply(fn, args) => loop(fn, argss)
692-
case _ => argss
693-
loop(tree, Nil)
694-
695-
/** The term arguments of a possibly curried call */
696-
def termArgss(tree: Tree): List[List[Tree]] =
697-
@tailrec
698-
def loop(tree: Tree, argss: List[List[Tree]]): List[List[Tree]] = tree match
699-
case Apply(fn, args) => loop(fn, args :: argss)
700-
case TypeApply(fn, args) => loop(fn, argss)
701-
case _ => argss
702-
loop(tree, Nil)
703-
704830
/** The type and term arguments of a possibly curried call, in the order they are given */
705831
def allArgss(tree: Tree): List[List[Tree]] =
706832
@tailrec

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ private sealed trait WarningSettings:
159159

160160
val Whelp: Setting[Boolean] = BooleanSetting("-W", "Print a synopsis of warning options.")
161161
val XfatalWarnings: Setting[Boolean] = BooleanSetting("-Werror", "Fail the compilation if there are any warnings.", aliases = List("-Xfatal-warnings"))
162+
val WvalueDiscard: Setting[Boolean] = BooleanSetting("-Wvalue-discard", "Warn when non-Unit expression results are unused.")
162163

163164
val Wunused: Setting[List[ChoiceWithHelp[String]]] = MultiChoiceHelpSetting(
164165
name = "-Wunused",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
188188
case MissingImplicitArgumentID // errorNumber 172
189189
case CannotBeAccessedID // errorNumber 173
190190
case InlineGivenShouldNotBeFunctionID // errorNumber 174
191+
case ValueDiscardingID // errorNumber 175
191192

192193
def errorNumber = ordinal - 1
193194

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2790,3 +2790,8 @@ extends SyntaxMsg(InlineGivenShouldNotBeFunctionID):
27902790
| inline def apply(x: A) = x.toB
27912791
"""
27922792

2793+
class ValueDiscarding(tp: Type)(using Context)
2794+
extends Message(ValueDiscardingID):
2795+
def kind = MessageKind.PotentialIssue
2796+
def msg(using Context) = i"discarded non-Unit value of type $tp"
2797+
def explain(using Context) = ""

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3944,6 +3944,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
39443944
// so will take the code path that decides on inlining
39453945
val tree1 = adapt(tree, WildcardType, locked)
39463946
checkStatementPurity(tree1)(tree, ctx.owner)
3947+
if (!ctx.isAfterTyper && !tree.isInstanceOf[Inlined] && ctx.settings.WvalueDiscard.value && !isThisTypeResult(tree)) {
3948+
report.warning(ValueDiscarding(tree.tpe), tree.srcPos)
3949+
}
39473950
return tpd.Block(tree1 :: Nil, Literal(Constant(())))
39483951
}
39493952

tests/neg/warn-value-discard.check

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- [E175] Potential Issue Error: tests/neg/warn-value-discard.scala:15:35 ----------------------------------------------
2+
15 | firstThing().map(_ => secondThing()) // error
3+
| ^^^^^^^^^^^^^
4+
| discarded non-Unit value of type Either[Failed, Unit]
5+
-- [E175] Potential Issue Error: tests/neg/warn-value-discard.scala:18:35 ----------------------------------------------
6+
18 | firstThing().map(_ => secondThing()) // error
7+
| ^^^^^^^^^^^^^
8+
| discarded non-Unit value of type Either[Failed, Unit]
9+
-- [E175] Potential Issue Error: tests/neg/warn-value-discard.scala:27:36 ----------------------------------------------
10+
27 | mutable.Set.empty[String].remove("") // error
11+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
| discarded non-Unit value of type Boolean
13+
-- [E175] Potential Issue Error: tests/neg/warn-value-discard.scala:39:41 ----------------------------------------------
14+
39 | mutable.Set.empty[String].subtractOne("") // error
15+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
16+
| discarded non-Unit value of type scala.collection.mutable.Set[String]
17+
-- [E175] Potential Issue Error: tests/neg/warn-value-discard.scala:59:4 -----------------------------------------------
18+
59 | mutable.Set.empty[String] += "" // error
19+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
20+
| discarded non-Unit value of type scala.collection.mutable.Set[String]

tests/neg/warn-value-discard.scala

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// scalac: -Wvalue-discard -Werror
2+
3+
import scala.util.{Either, Right, Left}
4+
import scala.collection.mutable
5+
6+
case class Failed(msg: String)
7+
8+
def firstThing(): Either[Failed, Unit] =
9+
Right(())
10+
11+
def secondThing(): Either[Failed, Unit] =
12+
Left(Failed("whoops you should have flatMapped me"))
13+
14+
def singleExpr(): Either[Failed, Unit] =
15+
firstThing().map(_ => secondThing()) // error
16+
17+
def block(): Either[Failed, Unit] = {
18+
firstThing().map(_ => secondThing()) // error
19+
}
20+
21+
class ValueDiscardTest:
22+
val field = mutable.Set.empty[String]
23+
24+
def remove(): Unit =
25+
// Set#remove returns a Boolean, not this.type
26+
// --> Warning
27+
mutable.Set.empty[String].remove("") // error
28+
29+
// TODO IMHO we don't need to support this,
30+
// as it's just as easy to add a @nowarn annotation as a Unit ascription
31+
//def removeAscribed(): Unit = {
32+
// mutable.Set.empty[String].remove(""): Unit // nowarn
33+
//}
34+
35+
def subtract(): Unit =
36+
// - Set#subtractOne returns this.type
37+
// - receiver is not a field or a local variable (not quite sure what you'd call it)
38+
// --> Warning
39+
mutable.Set.empty[String].subtractOne("") // error
40+
41+
def mutateLocalVariable(): Unit = {
42+
// - Set#subtractOne returns this.type
43+
// - receiver is a local variable
44+
// --> No warning
45+
val s: mutable.Set[String] = mutable.Set.empty[String]
46+
s.subtractOne("")
47+
}
48+
49+
def mutateField(): Unit =
50+
// - Set#subtractOne returns this.type
51+
// - receiver is a local variable
52+
// --> No warning
53+
field.subtractOne("")
54+
55+
def assignmentOperator(): Unit =
56+
// - += returns this.type
57+
// - receiver is not a field or a local variable
58+
// --> Warning
59+
mutable.Set.empty[String] += "" // error
60+
61+
def assignmentOperatorLocalVariable(): Unit =
62+
// - += returns this.type
63+
// - receiver is a local variable
64+
// --> No warning
65+
val s: mutable.Set[String] = mutable.Set.empty[String]
66+
s += ""

0 commit comments

Comments
 (0)