Skip to content

Commit 240ada0

Browse files
committed
Warn unused unused annotation
1 parent 18e2487 commit 240ada0

File tree

12 files changed

+143
-53
lines changed

12 files changed

+143
-53
lines changed

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ lazy val compiler = configureAsSubproject(project)
489489
Compile / scalacOptions ++= Seq(
490490
"-Xlint",
491491
"-feature",
492+
//"-Wconf:cat=unused-unused&site=scala.tools.nsc.reporters.Reporter.info.force:s",
492493
"-Wconf:cat=deprecation&msg=early initializers:s", // compiler heavily relies upon early initializers
493494
),
494495
Compile / doc / scalacOptions ++= Seq(

src/compiler/scala/tools/nsc/Reporting.scala

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import scala.reflect.internal.util.StringOps.countElementsAsString
2121
import scala.reflect.internal.util.{NoSourceFile, Position, SourceFile}
2222
import scala.tools.nsc.Reporting.Version.{NonParseableVersion, ParseableVersion}
2323
import scala.tools.nsc.Reporting._
24+
import scala.tools.nsc.settings.ScalaVersion
2425
import scala.util.matching.Regex
2526

2627
/** Provides delegates to the reporter doing the actual work.
@@ -80,15 +81,25 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
8081
suppressions.getOrElse(pos.source, Nil).exists(_.annotPos.point == pos.point)
8182

8283
def runFinished(hasErrors: Boolean): Unit = {
84+
val warningNowarn = settings.warnUnusedNowarn
85+
val warningUnused = settings.warnUnusedUnused && ScalaVersion.current >= ScalaVersion("2.13.11")
86+
def maybeWarn(s: Suppression): Unit =
87+
if (!s.used)
88+
if (!s.synthetic) {
89+
if (warningNowarn)
90+
issueWarning(Message.Plain(s.annotPos, "@nowarn annotation does not suppress any warnings", WarningCategory.UnusedNowarn, ""))
91+
}
92+
else if (warningUnused && s.filters.exists { case MessageFilter.Category(WarningCategory.Unused) => true case _ => false })
93+
issueWarning(Message.Plain(s.annotPos, "@unused annotation does not suppress any warnings", WarningCategory.UnusedUnused, ""))
8394
// report suspended messages (in case the run finished before typer)
8495
suspendedMessages.valuesIterator.foreach(_.foreach(issueWarning))
8596
// report unused nowarns only if all all phases are done. scaladoc doesn't run all phases.
86-
if (!hasErrors && settings.warnUnusedNowarn && !settings.isScaladoc)
97+
if (!hasErrors && (warningNowarn || warningUnused) && !settings.isScaladoc)
8798
for {
8899
source <- suppressions.keysIterator.toList
89100
sups <- suppressions.remove(source)
90101
sup <- sups.reverse
91-
} if (!sup.used && !sup.synthetic) issueWarning(Message.Plain(sup.annotPos, "@nowarn annotation does not suppress any warnings", WarningCategory.UnusedNowarn, ""))
102+
} maybeWarn(sup)
92103
}
93104

94105
def reportSuspendedMessages(unit: CompilationUnit): Unit = {
@@ -257,7 +268,7 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
257268
} else warning(pos, msg, featureCategory(featureTrait.nameString), site)
258269
}
259270

260-
// Used in the optimizer where we don't have no symbols, the site string is created from the class internal name and method name.
271+
// Used in the optimizer where we have no symbols, the site string is created from the class internal name and method name.
261272
def warning(pos: Position, msg: String, category: WarningCategory, site: String): Unit =
262273
issueIfNotSuppressed(Message.Plain(pos, msg, category, site))
263274

@@ -380,6 +391,7 @@ object Reporting {
380391
object UnusedLocals extends Unused; add(UnusedLocals)
381392
object UnusedParams extends Unused; add(UnusedParams)
382393
object UnusedNowarn extends Unused; add(UnusedNowarn)
394+
object UnusedUnused extends Unused; add(UnusedUnused)
383395

384396
sealed trait Lint extends WarningCategory { override def summaryCategory: WarningCategory = Lint }
385397
object Lint extends Lint { override def includes(o: WarningCategory): Boolean = o.isInstanceOf[Lint] }; add(Lint)

src/compiler/scala/tools/nsc/ast/TreeDSL.scala

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
package scala.tools.nsc
1414
package ast
1515

16-
import scala.annotation.unused
1716
import scala.language.implicitConversions
1817

1918
/** A DSL for generating scala code. The goal is that the
@@ -79,13 +78,9 @@ trait TreeDSL {
7978
def INT_- (other: Tree) = fn(target, getMember(IntClass, nme.MINUS), other)
8079

8180
// generic operations on ByteClass, IntClass, LongClass
82-
@unused("avoid warning for multiple parameters")
8381
def GEN_| (other: Tree, kind: ClassSymbol) = fn(target, getMember(kind, nme.OR), other)
84-
@unused("avoid warning for multiple parameters")
8582
def GEN_& (other: Tree, kind: ClassSymbol) = fn(target, getMember(kind, nme.AND), other)
86-
@unused("avoid warning for multiple parameters")
8783
def GEN_== (other: Tree, kind: ClassSymbol) = fn(target, getMember(kind, nme.EQ), other)
88-
@unused("avoid warning for multiple parameters")
8984
def GEN_!= (other: Tree, kind: ClassSymbol) = fn(target, getMember(kind, nme.NE), other)
9085

9186
/** Apply, Select, Match **/

src/compiler/scala/tools/nsc/settings/ScalaVersion.scala

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,14 @@ object ScalaVersion {
126126
}
127127
}
128128

129-
/**
130-
* Represents the data after the dash in major.minor.rev-build
129+
/** Represents the data after the dash in "major.minor.rev-build".
130+
*
131+
* Builds are ordered from Development, Milestone, RC, to Final.
132+
*
133+
* Development builds are ordered on their arbitrary string ID
134+
* (which may be a timestamp and git hash, such as "20230101-003230-4d2b33e").
135+
*
136+
* Other builds are numbered (M0, M1, RC0, RC1).
131137
*/
132138
abstract class ScalaBuild extends Ordered[ScalaBuild] {
133139
/**
@@ -136,62 +142,54 @@ abstract class ScalaBuild extends Ordered[ScalaBuild] {
136142
*/
137143
def unparse: String
138144
}
139-
/**
140-
* A development, test, integration, snapshot or other "unofficial" build
145+
/** A development, test, integration, snapshot or other "unofficial" build.
141146
*/
142147
case class Development(id: String) extends ScalaBuild {
143148
def unparse = s"-${id}"
144149

145150
def compare(that: ScalaBuild) = that match {
146-
// sorting two development builds based on id is reasonably valid for two versions created with the same schema
147-
// otherwise it's not correct, but since it's impossible to put a total ordering on development build versions
148-
// this is a pragmatic compromise
151+
// sorting two development builds based on id is reasonably valid
152+
// if the two version strings were created with the same schema.
149153
case Development(thatId) => id compare thatId
150-
// assume a development build is newer than anything else, that's not really true, but good luck
151-
// mapping development build versions to other build types
152-
case _ => 1
154+
// assume a development build is older than anything else.
155+
case _ => -1
153156
}
154157
}
155-
/**
156-
* A final final
158+
/** A final final.
157159
*/
158160
case object Final extends ScalaBuild {
159161
def unparse = ""
160162

161163
def compare(that: ScalaBuild) = that match {
162164
case Final => 0
163-
// a final is newer than anything other than a development build or another final
164-
case Development(_) => -1
165+
// a final is newer than anything else.
165166
case _ => 1
166167
}
167168
}
168169

169-
/**
170-
* A candidate for final release
170+
/** A candidate for final release.
171171
*/
172172
case class RC(n: Int) extends ScalaBuild {
173173
def unparse = s"-RC${n}"
174174

175175
def compare(that: ScalaBuild) = that match {
176+
case Final => -1
176177
// compare two rcs based on their RC numbers
177178
case RC(thatN) => n - thatN
178-
// an rc is older than anything other than a milestone or another rc
179-
case Milestone(_) => 1
180-
case _ => -1
179+
// an rc is newer than anything else
180+
case _ => 1
181181
}
182182
}
183183

184-
/**
185-
* An intermediate release
184+
/** An intermediate release.
186185
*/
187186
case class Milestone(n: Int) extends ScalaBuild {
188187
def unparse = s"-M${n}"
189188

190189
def compare(that: ScalaBuild) = that match {
191190
// compare two milestones based on their milestone numbers
192191
case Milestone(thatN) => n - thatN
193-
// a milestone is older than anything other than another milestone
192+
case Development(_) => 1
194193
case _ => -1
195-
196194
}
197195
}

src/compiler/scala/tools/nsc/settings/Warnings.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,9 @@ trait Warnings {
144144
val Implicits = Choice("implicits", "Warn if an implicit parameter is unused.")
145145
val Synthetics = Choice("synthetics", "Warn if a synthetic implicit parameter (context bound) is unused.")
146146
val Nowarn = Choice("nowarn", "Warn if a @nowarn annotation does not suppress any warnings.")
147+
val Unused = Choice("unused", "Warn if an @unused annotation does not suppress any warnings.")
147148
val Params = Choice("params", "Enable -Wunused:explicits,implicits,synthetics.", expandsTo = List(Explicits, Implicits, Synthetics))
148-
val Linted = Choice("linted", "-Xlint:unused.", expandsTo = List(Imports, Privates, Locals, Implicits, Nowarn))
149+
val Linted = Choice("linted", "-Xlint:unused.", expandsTo = List(Imports, Privates, Locals, Implicits, Nowarn, Unused))
149150
}
150151

151152
// The -Ywarn-unused warning group.
@@ -166,6 +167,7 @@ trait Warnings {
166167
def warnUnusedImplicits = warnUnused contains UnusedWarnings.Implicits
167168
def warnUnusedSynthetics = warnUnused contains UnusedWarnings.Synthetics
168169
def warnUnusedNowarn = warnUnused contains UnusedWarnings.Nowarn
170+
def warnUnusedUnused = warnUnused contains UnusedWarnings.Unused
169171

170172
val warnExtraImplicit = BooleanSetting("-Wextra-implicit", "Warn when more than one implicit parameter section is defined.") withAbbreviation "-Ywarn-extra-implicit"
171173

src/compiler/scala/tools/nsc/typechecker/TypeDiagnostics.scala

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import scala.util.control.Exception.ultimately
2020
import symtab.Flags._
2121
import PartialFunction.{cond, condOpt}
2222
import scala.annotation.{nowarn, tailrec}
23-
import scala.tools.nsc.Reporting.WarningCategory
23+
import scala.tools.nsc.Reporting.{MessageFilter, Suppression, WarningCategory}
2424

2525
/** An interface to enable higher configurability of diagnostic messages
2626
* regarding type errors. This is barely a beginning as error messages are
@@ -500,10 +500,20 @@ trait TypeDiagnostics extends splain.SplainDiagnostics {
500500
val ignoreNames: Set[TermName] = Set(
501501
"readResolve", "readObject", "writeObject", "writeReplace"
502502
).map(TermName(_))
503+
504+
// is this a getter-setter pair? cf g.setterIn(g.owner) == s
505+
def sameReference(g: Symbol, s: Symbol) =
506+
if (g.accessed.exists && s.accessed.exists) g.accessed == s.accessed
507+
else g.owner == s.owner && g.setterName == s.name //sympos(g) == sympos(s)
508+
509+
def sympos(s: Symbol): Int =
510+
if (s.pos.isDefined) s.pos.point else if (s.isTerm) s.asTerm.referenced.pos.point else -1
511+
def treepos(t: Tree): Int =
512+
if (t.pos.isDefined) t.pos.point else sympos(t.symbol)
503513
}
504514

505515
class UnusedPrivates extends Traverser {
506-
import UnusedPrivates.ignoreNames
516+
import UnusedPrivates._
507517
def isEffectivelyPrivate(sym: Symbol): Boolean = false
508518
val defnTrees = ListBuffer[MemberDef]()
509519
val targets = mutable.Set[Symbol]()
@@ -538,6 +548,11 @@ trait TypeDiagnostics extends splain.SplainDiagnostics {
538548

539549
override def traverse(t: Tree): Unit = {
540550
val sym = t.symbol
551+
if (sym != null && sym.pos.isDefined)
552+
sym.getAnnotation(UnusedClass).foreach { annot =>
553+
if (!runReporting.suppressionExists(annot.pos))
554+
runReporting.addSuppression(Suppression(annot.pos, List(MessageFilter.Category(WarningCategory.Unused)), sym.pos.start, sym.pos.end, synthetic = true))
555+
}
541556
t match {
542557
case m: MemberDef if qualifies(sym) && !t.isErrorTyped =>
543558
t match {
@@ -594,10 +609,8 @@ trait TypeDiagnostics extends splain.SplainDiagnostics {
594609
}
595610
super.traverse(t)
596611
}
597-
def isSuppressed(sym: Symbol): Boolean = sym.hasAnnotation(UnusedClass)
598612
def isUnusedType(m: Symbol): Boolean = (
599613
m.isType
600-
&& !isSuppressed(m)
601614
&& !m.isTypeParameterOrSkolem // would be nice to improve this
602615
&& (m.isPrivate || m.isLocalToBlock || isEffectivelyPrivate(m))
603616
&& !(treeTypes.exists(_.exists(_.typeSymbolDirect == m)))
@@ -612,7 +625,6 @@ trait TypeDiagnostics extends splain.SplainDiagnostics {
612625
}
613626
def isUnusedTerm(m: Symbol): Boolean = (
614627
m.isTerm
615-
&& !isSuppressed(m)
616628
&& (!m.isSynthetic || isSyntheticWarnable(m))
617629
&& ((m.isPrivate && !(m.isConstructor && m.owner.isAbstract)) || m.isLocalToBlock || isEffectivelyPrivate(m))
618630
&& !targets(m)
@@ -631,26 +643,16 @@ trait TypeDiagnostics extends splain.SplainDiagnostics {
631643
&& s.name == m.name && s.owner.isConstructor && s.owner.owner == m.owner) // exclude ctor params
632644
))
633645
)
634-
def sympos(s: Symbol): Int =
635-
if (s.pos.isDefined) s.pos.point else if (s.isTerm) s.asTerm.referenced.pos.point else -1
636-
def treepos(t: Tree): Int =
637-
if (t.pos.isDefined) t.pos.point else sympos(t.symbol)
638-
639646
def unusedTypes = defnTrees.toList.filter(t => isUnusedType(t.symbol)).sortBy(treepos)
640647
def unusedTerms = {
641648
val all = defnTrees.toList.filter(v => isUnusedTerm(v.symbol))
642649

643-
// is this a getter-setter pair? and why is this a difficult question for traits?
644-
def sameReference(g: Symbol, s: Symbol) =
645-
if (g.accessed.exists && s.accessed.exists) g.accessed == s.accessed
646-
else g.owner == s.owner && g.setterName == s.name //sympos(g) == sympos(s)
647-
648650
// filter out setters if already warning for getter.
649651
val clean = all.filterNot(v => v.symbol.isSetter && all.exists(g => g.symbol.isGetter && sameReference(g.symbol, v.symbol)))
650652
clean.sortBy(treepos)
651653
}
652654
// local vars which are never set, except those already returned in unused
653-
def unsetVars = varsWithoutSetters.filter(v => !isSuppressed(v) && !setVars(v) && !isUnusedTerm(v)).toList.sortBy(sympos)
655+
def unsetVars = varsWithoutSetters.filter(v => !setVars(v) && !isUnusedTerm(v)).toList.sortBy(sympos)
654656
def unusedParams = params.iterator.filter(isUnusedParam).toList.sortBy(sympos)
655657
def inDefinedAt(p: Symbol) = p.owner.isMethod && p.owner.name == nme.isDefinedAt && p.owner.owner.isAnonymousFunction
656658
def unusedPatVars = patvars.toList.filter(p => isUnusedTerm(p) && !inDefinedAt(p)).sortBy(sympos)
@@ -681,7 +683,8 @@ trait TypeDiagnostics extends splain.SplainDiagnostics {
681683

682684
// `checkUnused` is invoked after type checking. we have to avoid using `typer.context.warning`, which uses
683685
// `context.owner` as the `site` of the warning, but that's the root symbol at this point.
684-
def emitUnusedWarning(pos: Position, msg: String, category: WarningCategory, site: Symbol): Unit = runReporting.warning(pos, msg, category, site)
686+
def emitUnusedWarning(pos: Position, msg: String, category: WarningCategory, site: Symbol): Unit =
687+
runReporting.warning(pos, msg, category, site)
685688

686689
def run(unusedPrivates: UnusedPrivates)(body: Tree): Unit = {
687690
unusedPrivates.traverse(body)
@@ -723,9 +726,8 @@ trait TypeDiagnostics extends splain.SplainDiagnostics {
723726
for (defn <- unusedPrivates.unusedTerms if shouldWarnOn(defn.symbol)) { termWarning(defn) }
724727
for (defn <- unusedPrivates.unusedTypes if shouldWarnOn(defn.symbol)) { typeWarning(defn) }
725728

726-
for (v <- unusedPrivates.unsetVars) {
729+
for (v <- unusedPrivates.unsetVars)
727730
emitUnusedWarning(v.pos, s"local var ${v.name} in ${v.owner} ${valAdvice}", WarningCategory.UnusedPrivates, v)
728-
}
729731
}
730732
if (settings.warnUnusedPatVars) {
731733
for (v <- unusedPrivates.unusedPatVars)
@@ -774,7 +776,6 @@ trait TypeDiagnostics extends splain.SplainDiagnostics {
774776
}
775777
}
776778

777-
778779
trait TyperDiagnostics {
779780
self: Typer =>
780781

src/library/scala/collection/mutable/StringBuilder.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import scala.Predef.{ // unimport char-related implicit conversions to avoid tri
2121
genericWrapArray => _,
2222
wrapCharArray => _,
2323
wrapString => _,
24-
//_
24+
//_ : @unused // note that Predef is unimported already by single masking import
2525
}
2626

2727
/** A builder for mutable sequence of characters. This class provides an API

test/files/neg/t11631.check

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
t11631.scala:13: warning: private method z in class C is never used
2+
private def z = 37 // unused
3+
^
4+
t11631.scala:18: warning: local var w in method q is never used
5+
var w = 27 // unused
6+
^
7+
t11631.scala:9: warning: @unused annotation does not suppress any warnings
8+
def g(@unused i: Int) = i + 1 // unused unused
9+
^
10+
t11631.scala:11: warning: @unused annotation does not suppress any warnings
11+
@unused private def x = 42 // unused unused
12+
^
13+
t11631.scala:17: warning: @unused annotation does not suppress any warnings
14+
@unused var v = 42 // unused unused
15+
^
16+
error: No warnings can be incurred under -Werror.
17+
5 warnings
18+
1 error

test/files/neg/t11631.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
// scalac: -Xsource:3 -Werror -Wunused
3+
4+
import annotation.*
5+
6+
class C {
7+
def n: Int = 42
8+
def f(@unused i: Int) = n + 1
9+
def g(@unused i: Int) = i + 1 // unused unused
10+
11+
@unused private def x = 42 // unused unused
12+
@unused private def y = 27
13+
private def z = 37 // unused
14+
def p() = println(x)
15+
16+
def q() = {
17+
@unused var v = 42 // unused unused
18+
var w = 27 // unused
19+
v += 1 // write it
20+
println(v) // read it
21+
}
22+
}

test/files/neg/t11631b.check

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
t11631b.scala:5: warning: Unused import
2+
import annotation.*
3+
^
4+
t11631b.scala:9: warning: private object X in object D is never used
5+
private object X {
6+
^
7+
error: No warnings can be incurred under -Werror.
8+
2 warnings
9+
1 error

test/files/neg/t11631b.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
// scalac: -Xsource:3 -Werror -Wunused
3+
// debug: -Xsource:3 -Werror -Wunused -Vdebug -Vlog:_ -Vtyper
4+
5+
import annotation.*
6+
7+
object D {
8+
//@unused
9+
private object X {
10+
private final val x = 42
11+
}
12+
class X {
13+
def f = X.x
14+
}
15+
private object Y {
16+
private final val y = 42
17+
}
18+
class Y {
19+
import Y._
20+
def f = y
21+
}
22+
}

0 commit comments

Comments
 (0)