Skip to content

Commit

Permalink
apiStatus annotation
Browse files Browse the repository at this point in the history
This implements `apiStatus` annotation as a generic form of `deprecated` annotation. While deprecated is only able to discourage someone from using the some API, `apiStatus` can be more nuanced about the state (for instance Category.ApiMayChange), and choose the default compile-time actions (Action.Error, Action.Warning, etc). In other words, this gives library authors the lever to trigger compilation warning or
compilation errors!

One of the usage is to trigger compiler error from the library when a method is invoked to display migration message. Another usage would be to denote bincompat status of the API as warning.

This is a resend of scala#7790 based on the configurable warnings.
Ref scala#8373 / https://twitter.com/not_xuwei_k/status/1240354073297268737
  • Loading branch information
eed3si9n committed Mar 29, 2020
1 parent ff662eb commit a0f20e8
Show file tree
Hide file tree
Showing 12 changed files with 281 additions and 53 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ val mimaFilterSettings = Seq {
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.reflect.runtime.JavaUniverse#PerRunReporting.deprecationWarning"),
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.nowarn$"),
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.nowarn"),
ProblemFilters.exclude[MissingClassProblem]("scala.annotation.apiStatus*"),
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.reflect.runtime.Settings#*.clearSetByUser"),

////////////////////////////////////////////////////////////////////////////// Vector backward compatiblity
Expand Down
156 changes: 107 additions & 49 deletions src/compiler/scala/tools/nsc/Reporting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package scala
package tools
package nsc

import java.util.Locale.ENGLISH
import java.util.regex.PatternSyntaxException

import scala.collection.mutable
Expand Down Expand Up @@ -80,7 +81,7 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
val sources = suppressions.keysIterator.toList
for (source <- sources; sups <- suppressions.remove(source); sup <- sups.reverse) {
if (!sup.used)
issueWarning(Message.Plain(sup.annotPos, "@nowarn annotation does not suppress any warnings", WarningCategory.UnusedNowarn, ""))
issueWarning(Message.Plain(sup.annotPos, "@nowarn annotation does not suppress any warnings", WarningCategory.UnusedNowarn))
}
}
}
Expand All @@ -104,28 +105,37 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
sm.getOrElseUpdate(category, mutable.LinkedHashMap.empty)
}

private def issueWarning(warning: Message): Unit = {
private def issueWarning(warning: Message): Unit = issueWarning(warning, wconf.action(warning))

private def issueWarning(warning: Message, action: Action): Unit = {
def verboseMessage(category: WarningCategory, msg: String, site: String, origin: String, version: Version): String =
s"[${category.name} @ $site" +
(if (origin != "") s" | origin=$origin" else "") +
(if (version.filterString != "") s" | version=${version.filterString}" else "") +
s"] $msg"
def verbose = warning match {
case Message.Deprecation(_, msg, site, origin, version) => s"[${warning.category.name} @ $site | origin=$origin | version=${version.filterString}] $msg"
case Message.Plain(_, msg, category, site) => s"[${category.name} @ $site] $msg"
case Message.Deprecation(_, msg, site, origin, version) => verboseMessage(warning.category, msg, site, origin, version)
case Message.Plain(_, msg, category, site, origin, version) => verboseMessage(category, msg, site, origin, version)
}
wconf.action(warning) match {
case Action.Error => reporter.error(warning.pos, warning.msg)
case Action.Warning => reporter.warning(warning.pos, warning.msg)
action match {
case Action.Error => reporter.error(warning.pos, warning.msg)
case Action.Warning => reporter.warning(warning.pos, warning.msg)
case Action.WarningVerbose => reporter.warning(warning.pos, verbose)
case Action.Info => reporter.echo(warning.pos, warning.msg)
case Action.InfoVerbose => reporter.echo(warning.pos, verbose)
case Action.Info => reporter.echo(warning.pos, warning.msg)
case Action.InfoVerbose => reporter.echo(warning.pos, verbose)
case a @ (Action.WarningSummary | Action.InfoSummary) =>
val m = summaryMap(a, warning.category.summaryCategory)
if (!m.contains(warning.pos)) m.addOne((warning.pos, warning))
case Action.Silent =>
}
}

private def checkSuppressedAndIssue(warning: Message): Unit = {
private def checkSuppressedAndIssue(warning: Message): Unit = checkSuppressedAndIssue(warning, wconf.action(warning))

private def checkSuppressedAndIssue(warning: Message, action: Action): Unit = {
if (suppressionsComplete) {
if (!isSuppressed(warning))
issueWarning(warning)
issueWarning(warning, action)
} else
suspendedMessages += warning
}
Expand Down Expand Up @@ -245,12 +255,37 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio

// 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 =
checkSuppressedAndIssue(Message.Plain(pos, msg, category, site))
checkSuppressedAndIssue(Message.Plain(pos, msg, category, site, "", Version.fromString("")))

// Preferred over the overload above whenever a site symbol is available
def warning(pos: Position, msg: String, category: WarningCategory, site: Symbol): Unit =
warning(pos, msg, category, siteName(site))

// someone is using @apiStatus API
def handleApiStatus(pos: Position, sym: Symbol, site: Symbol): Unit = {
val category0 = sym.apiStatusCategory.getOrElse("unspecified")
val category = WarningCategory.parse(category0)
val message = sym.apiStatusMessage.getOrElse("")
val origin = siteName(sym)
val since = Version.fromString(sym.apiStatusVersion.getOrElse(""))
val msg = category match {
case WarningCategory.Deprecation =>
Message.Deprecation(pos, message, siteName(site), origin, since)
case _ =>
val sinceX = sym.apiStatusVersion match {
case Some(v) => s" ($v)"
case _ => ""
}
Message.Plain(pos, s"$message$sinceX", category, siteName(site), origin, since)
}
val defaultAction0 = sym.apiStatusDefaultAction.getOrElse("warning")
val defaultAction = Action.parse(defaultAction0) match {
case Right(a) => a
case Left(_) => Action.Warning
}
checkSuppressedAndIssue(msg, wconf.actionOpt(msg).getOrElse(defaultAction))
}

// used by Global.deprecationWarnings, which is used by sbt
def deprecationWarnings: List[(Position, String)] = summaryMap(Action.WarningSummary, WarningCategory.Deprecation).toList.map(p => (p._1, p._2.msg))
def uncheckedWarnings: List[(Position, String)] = summaryMap(Action.WarningSummary, WarningCategory.Unchecked).toList.map(p => (p._1, p._2.msg))
Expand Down Expand Up @@ -290,10 +325,15 @@ object Reporting {
def msg: String
def category: WarningCategory
def site: String // sym.FullName of the location where the warning is positioned, may be empty
def origin: String
}

object Message {
final case class Plain(pos: Position, msg: String, category: WarningCategory, site: String) extends Message
final case class Plain(pos: Position, msg: String, category: WarningCategory, site: String, origin: String, since: Version) extends Message
object Plain {
def apply(pos: Position, msg: String, category: WarningCategory): Plain =
Plain(pos, msg, category, "", "", Version.fromString(""))
}

// `site` and `origin` may be empty
final case class Deprecation(pos: Position, msg: String, site: String, origin: String, since: Version) extends Message {
Expand All @@ -302,12 +342,13 @@ object Reporting {
}

sealed trait WarningCategory {
lazy val name: String = {
def name: String = name0
private[this] lazy val name0: String = {
val objectName = this.getClass.getName.split('$').last
WarningCategory.insertDash.replaceAllIn(objectName, "-")
.stripPrefix("-")
.stripSuffix("-")
.toLowerCase
.toLowerCase(ENGLISH)
}

def includes(o: WarningCategory): Boolean = this eq o
Expand All @@ -317,8 +358,8 @@ object Reporting {
object WarningCategory {
private val insertDash = "(?=[A-Z][a-z])".r

var all: mutable.Map[String, WarningCategory] = mutable.Map.empty
private def add(c: WarningCategory): Unit = all += ((c.name, c))
val builtIn: mutable.Map[String, WarningCategory] = mutable.Map.empty
private def add(c: WarningCategory): Unit = builtIn += ((c.name, c))

object Deprecation extends WarningCategory; add(Deprecation)

Expand All @@ -330,6 +371,8 @@ object Reporting {

object JavaSource extends WarningCategory; add(JavaSource)

object Unspecified extends WarningCategory; add(Unspecified)

sealed trait Other extends WarningCategory { override def summaryCategory: WarningCategory = Other }
object Other extends Other { override def includes(o: WarningCategory): Boolean = o.isInstanceOf[Other] }; add(Other)
object OtherShadowing extends Other; add(OtherShadowing)
Expand Down Expand Up @@ -391,6 +434,20 @@ object Reporting {
object FeaturePostfixOps extends Feature; add(FeaturePostfixOps)
object FeatureReflectiveCalls extends Feature; add(FeatureReflectiveCalls)
object FeatureMacros extends Feature; add(FeatureMacros)

case class Custom private (override val name: String) extends WarningCategory {
override def includes(o: WarningCategory): Boolean = this == o
}
def custom(name: String): Custom = {
val n = WarningCategory.insertDash.replaceAllIn(name, "-")
.stripPrefix("-")
.stripSuffix("-")
.toLowerCase(ENGLISH)
Custom(n)
}

def parse(category: String): WarningCategory =
WarningCategory.builtIn.getOrElse(category, WarningCategory.custom(category))
}

sealed trait Version {
Expand Down Expand Up @@ -485,20 +542,19 @@ 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 _ => false
}
final case class OriginPattern(pattern: Regex) extends MessageFilter {
def matches(message: Message): Boolean = pattern.matches(message.origin)
}

final case class DeprecatedSince(comp: Int, version: ParseableVersion) extends MessageFilter {
final case class SincePattern(comp: Int, version: ParseableVersion) extends MessageFilter {
private[this] def isMatch(mv: ParseableVersion): Boolean =
if (comp == -1) mv.smaller(version)
else if (comp == 0) mv.same(version)
else mv.greater(version)
def matches(message: Message): Boolean = message match {
case Message.Deprecation(_, _, _, _, mv: ParseableVersion) =>
if (comp == -1) mv.smaller(version)
else if (comp == 0) mv.same(version)
else mv.greater(version)
case _ => false
case Message.Deprecation(_, _, _, _, mv: ParseableVersion) => isMatch(mv)
case Message.Plain(_, _, _, _, _, mv: ParseableVersion) => isMatch(mv)
case _ => false
}
}
}
Expand All @@ -514,17 +570,29 @@ object Reporting {
object InfoSummary extends Action
object InfoVerbose extends Action
object Silent extends Action

def parse(s: String): Either[String, Action] = s match {
case "error" | "e" => Right(Error)
case "warning" | "w" => Right(Warning)
case "warning-summary" | "ws" => Right(WarningSummary)
case "warning-verbose" | "wv" => Right(WarningVerbose)
case "info" | "i" => Right(Info)
case "info-summary" | "is" => Right(InfoSummary)
case "info-verbose" | "iv" => Right(InfoVerbose)
case "silent" | "s" => Right(Silent)
case _ => Left(s"unknonw action: `$s`")
}
}

final case class WConf(filters: List[(List[MessageFilter], Action)]) {
def action(message: Message): Action = filters.find(_._1.forall(_.matches(message))) match {
case Some((_, action)) => action
case _ => Action.Warning
def action(message: Message): Action = actionOpt(message).getOrElse(Action.Warning)

def actionOpt(message: Message): Option[Action] = filters.find(_._1.forall(_.matches(message))) map {
case (_, action) => action
}
}

object WConf {
import Action._
import MessageFilter._

private def regex(s: String) =
Expand All @@ -538,12 +606,11 @@ object Reporting {
regex(s.substring(4)).map(MessagePattern)
} else if (s.startsWith("cat=")) {
val cs = s.substring(4)
val c = WarningCategory.all.get(cs).map(Category)
c.toRight(s"Unknown category: `$cs`")
Right(Category(WarningCategory.parse(cs)))
} else if (s.startsWith("site=")) {
regex(s.substring(5)).map(SitePattern)
} else if (s.startsWith("origin=")) {
regex(s.substring(7)).map(DeprecatedOrigin)
regex(s.substring(7)).map(OriginPattern)
} else if(s.startsWith("since")) {
def fail = Left(s"invalid since filter: `$s`; required shape: `since<1.2.3`, `since=3.2`, `since>2`")
if (s.length < 6) fail
Expand All @@ -558,7 +625,7 @@ object Reporting {
(v, op) match {
case (_: NonParseableVersion, _) => fail
case (_, 99) => fail
case (pv: ParseableVersion, o) => Right(DeprecatedSince(o, pv))
case (pv: ParseableVersion, o) => Right(SincePattern(o, pv))
}
}
} else if (s.startsWith("src=")) {
Expand All @@ -577,26 +644,17 @@ object Reporting {
}

def parse(setting: List[String], rootDir: 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)
case "warning-verbose" | "wv" => Right(WarningVerbose)
case "info" | "i" => Right(Info)
case "info-summary" | "is" => Right(InfoSummary)
case "info-verbose" | "iv" => Right(InfoVerbose)
case "silent" | "s" => Right(Silent)
case _ => Left(List(s"unknonw action: `$s`"))
}

if (setting.isEmpty) Right(WConf(Nil))
else {
val parsedConfs: List[Either[List[String], (List[MessageFilter], Action)]] = setting.map(conf => {
val parts = conf.split("[&:]") // TODO: don't split on escaped \&
val (ms, fs) = parts.view.init.map(parseFilter(_, rootDir)).toList.partitionMap(identity)
if (ms.nonEmpty) Left(ms)
else if (fs.isEmpty) Left(List("no filters or no action defined"))
else parseAction(parts.last).map((fs, _))
else Action.parse(parts.last) match {
case Right(a) => Right((fs, a))
case Left(s) => Left(List(s))
}
})
val (ms, fs) = parsedConfs.partitionMap(identity)
if (ms.nonEmpty) Left(ms.flatten)
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/scala/tools/nsc/settings/Warnings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ trait Warnings {
| - silence certain deprecations: -Wconf:origin=some\\.lib\\..*&since>2.2:s
|
|Full list of message categories:
|${WarningCategory.all.keys.groupBy(_.split('-').head).toList.sortBy(_._1).map(_._2.toList.sorted.mkString(", ")).mkString(" - ", "\n - ", "")}
|${WarningCategory.builtIn.keys.groupBy(_.split('-').head).toList.sortBy(_._1).map(_._2.toList.sorted.mkString(", ")).mkString(" - ", "\n - ", "")}
|
|To suppress warnings locally, use the `scala.annotation.nowarn` annotation.
|
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/scala/tools/nsc/typechecker/RefChecks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,9 @@ abstract class RefChecks extends Transform {
// warnings after the first, but I think it'd be better if we didn't have to
// arbitrarily choose one as more important than the other.
private def checkUndesiredProperties(sym: Symbol, pos: Position): Unit = {
if (sym.isApiStatus && !currentOwner.ownerChain.exists(x => x.isApiStatus))
currentRun.reporting.handleApiStatus(pos, sym, currentOwner)

// If symbol is deprecated, and the point of reference is not enclosed
// in either a deprecated member or a scala bridge method, issue a warning.
if (sym.isDeprecated && !currentOwner.ownerChain.exists(x => x.isDeprecated))
Expand Down
Loading

0 comments on commit a0f20e8

Please sign in to comment.