Skip to content

Commit

Permalink
@silent annotation for local suppression, heavily inspired by silencer
Browse files Browse the repository at this point in the history
  • Loading branch information
lrytz committed Sep 5, 2019
1 parent 31cfce4 commit f9ed3a7
Show file tree
Hide file tree
Showing 12 changed files with 444 additions and 40 deletions.
3 changes: 3 additions & 0 deletions src/compiler/scala/tools/nsc/Global.scala
Expand Up @@ -1546,6 +1546,9 @@ class Global(var currentSettings: Settings, reporter0: Reporter)
if (settings.YstatisticsEnabled && settings.Ystatistics.contains(phase.name))
printStatisticsFor(phase)

if (!globalPhase.hasNext)
runReporting.warnUnusedSuppressions()

advancePhase()
}
profiler.finished()
Expand Down
112 changes: 85 additions & 27 deletions src/compiler/scala/tools/nsc/Reporting.scala
Expand Up @@ -16,7 +16,7 @@ package nsc

import scala.collection.mutable
import scala.reflect.internal.Symbols
import scala.reflect.internal.util.Position
import scala.reflect.internal.util.{Position, RangePosition, SourceFile}
import scala.reflect.internal.util.StringOps.countElementsAsString
import scala.tools.nsc.Reporting._
import scala.util.matching.Regex
Expand All @@ -43,6 +43,43 @@ trait Reporting extends scala.reflect.internal.Reporting { self: ast.Positions w
private val summarizedWarnings: mutable.Map[WarningCategory, mutable.LinkedHashMap[Position, Message]] = mutable.HashMap.empty
private val summarizedInfos: mutable.Map[WarningCategory, mutable.LinkedHashMap[Position, Message]] = mutable.HashMap.empty

private var suppressionsComplete = false
private val suppressions: mutable.LinkedHashMap[SourceFile, mutable.ListBuffer[Suppression]] = mutable.LinkedHashMap.empty
private val suspendedMessages: mutable.LinkedHashSet[Message] = mutable.LinkedHashSet.empty

private def isSuppressed(warning: Message): Boolean =
suppressions.getOrElse(warning.pos.source, Nil).find(_.matches(warning)) match {
case Some(s) => s.markUsed(); true
case _ => false
}

def addSuppression(sup: Suppression): Unit = {
val source = sup.annotPos.source
suppressions.getOrElseUpdate(source, mutable.ListBuffer.empty) += sup
}

def suppressionExists(pos: Position): Boolean =
suppressions.getOrElse(pos.source, Nil).exists(_.annotPos.point == pos.point)

def warnUnusedSuppressions(): Unit = {
val sources = suppressions.keysIterator.toList
for (source <- sources; sups <- suppressions.remove(source); sup <- sups.reverse) {
if (!sup.used)
issueWarning(Message.Plain(sup.annotPos,"@silent annotation does not suppress any warnings", WarningCategory.Other, ""))
}
}

def reportSuspendedMessages(): Unit = {
suppressionsComplete = true
// sort suppressions. they are not added in any particular order because of lazy type completion
suppressions.mapValuesInPlace((_, sups) => sups.sortBy(sup => 0 - sup.start))
suspendedMessages.foreach { m =>
if (!isSuppressed(m))
issueWarning(m)
}
suspendedMessages.clear()
}

private def summaryMap(action: Action, category: WarningCategory) = {
val sm = (action: @unchecked) match {
case Action.WarningSummary => summarizedWarnings
Expand All @@ -69,6 +106,14 @@ trait Reporting extends scala.reflect.internal.Reporting { self: ast.Positions w
}
}

private def checkSuppressedAndIssue(warning: Message): Unit = {
if (suppressionsComplete) {
if (!isSuppressed(warning))
issueWarning(warning)
} else
suspendedMessages += warning
}

private def summarize(action: Action, category: WarningCategory): Unit = {
def rerunMsg: String = {
val s: Settings#Setting = category match {
Expand Down Expand Up @@ -121,7 +166,7 @@ trait Reporting extends scala.reflect.internal.Reporting { self: ast.Positions w
private def siteName(s: Symbol) = if (s.exists) s.fullNameString else ""

def deprecationWarning(pos: Position, msg: String, since: String, site: String, origin: String): Unit =
issueWarning(Message.Deprecation(pos, msg, site, origin, new Version(since)))
checkSuppressedAndIssue(Message.Deprecation(pos, msg, site, origin, new Version(since)))

def deprecationWarning(pos: Position, origin: Symbol, site: Symbol, msg: String, since: String): Unit =
deprecationWarning(pos, msg, since, siteName(site), siteName(origin))
Expand Down Expand Up @@ -172,7 +217,7 @@ trait Reporting extends scala.reflect.internal.Reporting { self: ast.Positions w

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

// Preferred over the overload above whenever a site symbol is available
def warning(pos: Position, msg: String, category: WarningCategory, site: Symbol): Unit =
Expand Down Expand Up @@ -362,11 +407,11 @@ object Reporting {
}

final case class MessagePattern(pattern: Regex) extends MessageFilter {
def matches(message: Message): Boolean = pattern.matches(message.msg)
def matches(message: Message): Boolean = pattern.findFirstIn(message.msg).nonEmpty
}

final case class SitePattern(pattern: Regex) extends MessageFilter {
def matches(message: Message): Boolean = pattern.matches(message.site)
def matches(message: Message): Boolean = pattern.findFirstIn(message.site).nonEmpty
}

final case class SourcePattern(pattern: Regex) extends MessageFilter {
Expand All @@ -378,7 +423,7 @@ object Reporting {

final case class DeprecatedOrigin(pattern: Regex) extends MessageFilter {
def matches(message: Message): Boolean = message match {
case m: Message.Deprecation => pattern.matches(m.origin)
case m: Message.Deprecation => pattern.findFirstIn(m.origin).nonEmpty
case _ => false
}
}
Expand Down Expand Up @@ -418,25 +463,26 @@ object Reporting {
import Action._
import MessageFilter._

def parse(setting: List[String]): Either[List[String], WConf] = {
def filter(s: String): Either[String, MessageFilter] = {
if (s == "any") {
Right(Any)
} else if (s.startsWith("msg=")) {
Right(MessagePattern(s.substring(4).r))
} else if (s.startsWith("cat=")) {
val cs = s.substring(4)
val c = WarningCategory.all.get(cs).map(Category)
c.toRight(s"Unknown category: `$cs`")
} else if (s.startsWith("site=")) {
Right(SitePattern(s.substring(5).r))
} else if (s.startsWith("origin=")) {
Right(DeprecatedOrigin(s.substring(7).r))
} else {
Left(s"filter not yet implemented: $s")
}
def parseFilter(s: String): Either[String, MessageFilter] = {
if (s == "any") {
Right(Any)
} else if (s.startsWith("msg=")) {
Right(MessagePattern(s.substring(4).r)) // TODO: catch PatternSyntaxException
} else if (s.startsWith("cat=")) {
val cs = s.substring(4)
val c = WarningCategory.all.get(cs).map(Category)
c.toRight(s"Unknown category: `$cs`")
} else if (s.startsWith("site=")) {
Right(SitePattern(s.substring(5).r)) // TODO: catch PatternSyntaxException
} else if (s.startsWith("origin=")) {
Right(DeprecatedOrigin(s.substring(7).r)) // TODO: catch PatternSyntaxException
} else {
Left(s"filter not yet implemented: $s")
}
def action(s: String): Either[List[String], Action] = s match {
}

def parse(setting: List[String]): Either[List[String], WConf] = {
def parseAction(s: String): Either[List[String], Action] = s match {
case "error" | "e" => Right(Error)
case "warning" | "w" => Right(Warning)
case "warning-summary" | "ws" => Right(WarningSummary)
Expand All @@ -451,16 +497,28 @@ object Reporting {
if (setting.isEmpty) Right(WConf(Nil))
else {
val parsedConfs: List[Either[List[String], (List[MessageFilter], Action)]] = setting.map(conf => {
val parts = conf.split("[&:]")
val (ms, fs) = parts.view.init.map(filter).toList.partitionMap(identity)
val parts = conf.split("[&:]") // TODO: don't split on escaped \&
val (ms, fs) = parts.view.init.map(parseFilter).toList.partitionMap(identity)
if (ms.nonEmpty) Left(ms)
else if (fs.isEmpty) Left(List("no filters defined"))
else action(parts.last).map((fs, _))
else parseAction(parts.last).map((fs, _))
})
val (ms, fs) = parsedConfs.partitionMap(identity)
if (ms.nonEmpty) Left(ms.flatten)
else Right(WConf(fs))
}
}
}

case class Suppression(annotPos: Position, filters: List[MessageFilter], start: Int, end: Int) {
private[this] var _used = false
def used: Boolean = _used
def markUsed(): Unit = { _used = true }


def matches(message: Message): Boolean = {
val pos = message.pos
pos.isDefined && start <= pos.start && pos.end <= end && filters.exists(_.matches(message))
}
}
}
1 change: 1 addition & 0 deletions src/compiler/scala/tools/nsc/typechecker/Analyzer.scala
Expand Up @@ -108,6 +108,7 @@ trait Analyzer extends AnyRef
// defensive measure in case the bookkeeping in deferred macro expansion is buggy
clearDelayed()
if (StatisticsStatics.areSomeColdStatsEnabled) statistics.stopTimer(statistics.typerNanos, start)
runReporting.reportSuspendedMessages()
}
def apply(unit: CompilationUnit): Unit = {
try {
Expand Down
10 changes: 5 additions & 5 deletions src/compiler/scala/tools/nsc/typechecker/Namers.scala
Expand Up @@ -863,7 +863,7 @@ trait Namers extends MethodSynthesis {
// on these flag checks so it can't hurt.
def needsCycleCheck = sym.isNonClassType && !sym.isParameter && !sym.isExistential

val annotations = annotSig(tree.mods.annotations, _ => true)
val annotations = annotSig(tree.mods.annotations, tree, _ => true)

val tp = typeSig(tree, annotations)

Expand Down Expand Up @@ -931,7 +931,7 @@ trait Namers extends MethodSynthesis {
val pred: AnnotationInfo => Boolean =
if (isGetter) accessorAnnotsFilter(tree.mods)
else annotationFilter(FieldTargetClass, !mods.isParamAccessor)
annotSig(mods.annotations, pred)
annotSig(mods.annotations, tree, pred)
}

// must use typeSig, not memberSig (TODO: when do we need to switch namers?)
Expand Down Expand Up @@ -979,7 +979,7 @@ trait Namers extends MethodSynthesis {
val mods = valDef.mods
val annots =
if (mods.annotations.isEmpty) Nil
else annotSig(mods.annotations, accessorAnnotsFilter(valDef.mods, isSetter, isBean))
else annotSig(mods.annotations, valDef, accessorAnnotsFilter(valDef.mods, isSetter, isBean))

// for a setter, call memberSig to attribute the parameter (for a bean, we always use the regular method sig completer since they receive method types)
// for a regular getter, make sure it gets a NullaryMethodType (also, no need to recompute it: we already have the valSig)
Expand Down Expand Up @@ -1865,13 +1865,13 @@ trait Namers extends MethodSynthesis {
* they were added only in typer, depending on the compilation order, they may
* or may not be visible.
*/
def annotSig(annotations: List[Tree], pred: AnnotationInfo => Boolean): List[AnnotationInfo] =
def annotSig(annotations: List[Tree], annotee: Tree, pred: AnnotationInfo => Boolean): List[AnnotationInfo] =
annotations filterNot (_ eq null) map { ann =>
val ctx = typer.context
// need to be lazy, #1782. enteringTyper to allow inferView in annotation args, scala/bug#5892.
AnnotationInfo lazily {
enteringTyper {
val annotSig = newTyper(ctx.makeNonSilent(ann)) typedAnnotation ann
val annotSig = newTyper(ctx.makeNonSilent(ann)).typedAnnotation(ann, Some(annotee))
if (pred(annotSig)) annotSig else UnmappableAnnotation // UnmappableAnnotation will be dropped in typedValDef and typedDefDef
}
}
Expand Down
51 changes: 44 additions & 7 deletions src/compiler/scala/tools/nsc/typechecker/Typers.scala
Expand Up @@ -21,13 +21,13 @@ package tools.nsc
package typechecker

import scala.collection.mutable
import scala.reflect.internal.util.{FreshNameCreator, ListOfNil, Statistics, StatisticsStatics}
import scala.reflect.internal.util.{FreshNameCreator, ListOfNil, RangePosition, Statistics, StatisticsStatics}
import scala.reflect.internal.TypesStats
import mutable.ListBuffer
import symtab.Flags._
import Mode._
import PartialFunction.cond
import scala.tools.nsc.Reporting.WarningCategory
import scala.tools.nsc.Reporting.{MessageFilter, Suppression, WConf, WarningCategory}

// Suggestion check whether we can do without priming scopes with symbols of outer scopes,
// like the IDE does.
Expand Down Expand Up @@ -3837,17 +3837,54 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
/**
* Convert an annotation constructor call into an AnnotationInfo.
*/
def typedAnnotation(ann: Tree, mode: Mode = EXPRmode): AnnotationInfo = context.withinAnnotation {
def typedAnnotation(ann: Tree, annotee: Option[Tree], mode: Mode = EXPRmode): AnnotationInfo = context.withinAnnotation {
var hasError: Boolean = false
val pending = ListBuffer[AbsTypeError]()
def ErroneousAnnotation = new ErroneousAnnotation().setOriginal(ann)

def registerSilent(info: AnnotationInfo): Unit = {
if (annotee.nonEmpty && SilentClass.exists && info.matches(SilentClass) && !runReporting.suppressionExists(info.pos)) {
val filters = info.assocs match {
case Nil => List(MessageFilter.Any)
case (_, LiteralAnnotArg(s)) :: Nil =>
val (ms, fs) = s.stringValue.split('&').map(WConf.parseFilter).toList.partitionMap(identity)
if (ms.nonEmpty)
reporter.error(info.pos, s"Invalid message filter:\n${ms.mkString("\n")}")
fs
}
val (start, end) =
if (settings.Yrangepos) {
val p = annotee.get.pos
(p.start, p.end)
} else {
// compute approximate range
var s = unit.source.length
var e = 0
object setRange extends ForeachTreeTraverser({ child =>
val pos = child.pos
if (pos.isDefined) {
s = s min pos.start
e = e max pos.end
}
}) {
// in `@silent @ann(deprecatedMethod) def foo`, the deprecation warning should show
override def traverseModifiers(mods: Modifiers): Unit = ()
}
setRange(annotee.get)
(s, e max s)
}
runReporting.addSuppression(Suppression(info.pos, filters, start, end))
}
}

def finish(res: AnnotationInfo): AnnotationInfo = {
if (hasError) {
pending.foreach(ErrorUtils.issueTypeError)
ErroneousAnnotation
} else {
registerSilent(res)
res
}
else res
}

def reportAnnotationError(err: AbsTypeError) = {
Expand Down Expand Up @@ -3887,7 +3924,7 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
reportAnnotationError(ArrayConstantsError(tree)); None

case ann @ Apply(Select(New(tpt), nme.CONSTRUCTOR), args) =>
val annInfo = typedAnnotation(ann, mode)
val annInfo = typedAnnotation(ann, None, mode)
val annType = annInfo.atp

if (!annType.typeSymbol.isSubClass(pt.typeSymbol))
Expand Down Expand Up @@ -4475,7 +4512,7 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
if (arg1.isType) {
// make sure the annotation is only typechecked once
if (ann.tpe == null) {
val ainfo = typedAnnotation(ann, annotMode)
val ainfo = typedAnnotation(ann, Some(atd), annotMode)
val atype = arg1.tpe.withAnnotation(ainfo)

if (ainfo.isErroneous)
Expand All @@ -4492,7 +4529,7 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
}
else {
if (ann.tpe == null) {
val annotInfo = typedAnnotation(ann, annotMode)
val annotInfo = typedAnnotation(ann, Some(atd), annotMode)
ann setType arg1.tpe.withAnnotation(annotInfo)
}
val atype = ann.tpe
Expand Down
32 changes: 32 additions & 0 deletions src/library/scala/annotation/silent.scala
@@ -0,0 +1,32 @@
/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package scala.annotation

/** An annotation for local warning suppression.
*
* The optional `value` parameter allows selectively silencing messages, see `scalac -Wconf:help`
* for help. Examples:
*
* {{{
* def f = {
* 1: @silent // don't warn "a pure expression does nothing in statement position"
* 2
* }
*
* @silent def f = { 1; deprecated() } // don't warn
*
* @silent("msg=pure expression does nothing")
* def f = { 1; deprecated() } // show deprecation warning
* }}}
*/
class silent(value: String = "") extends ConstantAnnotation
2 changes: 1 addition & 1 deletion src/library/scala/annotation/tailrec.scala
Expand Up @@ -18,4 +18,4 @@ package scala.annotation
* If it is present, the compiler will issue an error if the method cannot
* be optimized into a loop.
*/
final class tailrec extends scala.annotation.StaticAnnotation
final class tailrec extends StaticAnnotation
1 change: 1 addition & 0 deletions src/reflect/scala/reflect/internal/Definitions.scala
Expand Up @@ -1270,6 +1270,7 @@ trait Definitions extends api.StandardDefinitions {
lazy val SwitchClass = requiredClass[scala.annotation.switch]
lazy val TailrecClass = requiredClass[scala.annotation.tailrec]
lazy val VarargsClass = requiredClass[scala.annotation.varargs]
lazy val SilentClass = getClassIfDefined("scala.annotation.silent")
lazy val uncheckedStableClass = requiredClass[scala.annotation.unchecked.uncheckedStable]
lazy val uncheckedVarianceClass = requiredClass[scala.annotation.unchecked.uncheckedVariance]

Expand Down

0 comments on commit f9ed3a7

Please sign in to comment.