I think (without much empirical evidence) that the build is the best point to intervene in the natural order of things.
Usually, we prefer local solutions, such as keeping unit tests close to the code under test, because the person writing the code has the most knowledge about it.
If the code is known to be “dirty”, then annotating it @nowarn
spares other people from that terrible knowledge.
But the coder doesn’t really know about versioning constraints, let alone future versions.
If someone comes along and bumps a version in the build, and suddenly there are warnings, but it’s determined that the warnings are “expected”, which is to say, “benign”, then they should managed at the point of change, in the build.
I see the code has nowarn annotations for unused imports, for example. That might be a good candidate for decluttering the code in favor of sweeping it under the rug of build configuration.
The unused imports can be selected by src
file, site
of the enclosing element, and origin
to mean the import “selector” that causes the warning. For example, for import collection.{immutable, mutable}
, you might specify origin=scala.collection.mutable
. (In the following, origin is a regex so the star is glob, not the import syntax. origin=scala.collection\._
matches collection._
and collection.*
.)
➜ scalac -d /tmp -Xsource:3 -Xlint "-Wconf:cat=unused-imports&site=example.C:e,cat=unused-imports&origin=scala.collection\..*&src=unused-import.scala:i" unused-import.scala
unused-import.scala:7: error: Unused import
import concurrent.*
^
1 error
to distinguish these imports
package example
import collection.*
class C {
import concurrent.*
def p() = println("hello, world")
}
The same considerations apply to nowarn.
There is precedent for versioning because deprecations know when they were deprecated. For usages of this API:
class D {
@deprecated("old stuff", since="0.1")
def d() = println("old stuff")
@deprecated("newer stuff", since="Dlib 0.2")
def e() = println("newer stuff")
}
the deprecations can be selected by version with a simply clever syntax:
"-Wconf:cat=deprecation&origin=example\.D\..*&since>0.1:s"
The origin is a regex, so literal dot is escaped, and the version string is snipped from the since string.
Then similar syntax could be adopted for nowarn:
"-Wconf:cat=unused-nowarn&origin=example\.D\..*&since>2.12:s"
But this would only control for versions of the Scala compiler and libraries, which is a narrow restriction.
For example, suppose D
is amended in Dlib 1.0
with a better signature:
def f(): this.type = this
//def f(): D = this
where the old API warns under -Wnonunit-statement
for a usage def run(): Unit = d.f()
.
Currently, the build (which knows about versions) does not communicate with Wconf
. If it did, I could -Wconf:cat=unused-nowarn&since<Dlib 1.0
or similarly for cat=w-flag-value-discard
.
In sum, I am suspicious of sprinkling warning suppression everywhere, without an ability to audit what was suppressed. The purpose of inline @nowarn
is not to say, “Nothing to see here,” but to warn that something is so terrible that you don’t want to know. (I do like it for special usages, such as tests of deprecated API, where suppression is locally known to be justified.)
The purpose of -Wconf
is to externalize versioning concerns about warnings, especially under -Werror
.
It’s nice to avoid cracking open a source file whenever messaging changes due to unrelated causes.
I also found @SuppressWarnings
an annoyance, since it warned variously depending on what tool was processing the annotation.
It’s worth mentioning the dotty project guidance of minimizing annotations such as @tailrec
, which is clutter unless there is a practical danger of stack overflow.
This post is just an excuse for me to learn more about how -Wconf
works (in Scala 2); its length should not be taken to mean I’ve given the topic a lot of thought.