Skip to content

Commit

Permalink
Reorganize RefChecks tasks
Browse files Browse the repository at this point in the history
 - Split out forward reference checks and cross version (i.e. experimental/deprecated) checks
   into their own miniphases. These have nothing to do with the core tasks of RefChecks.
 - Move RefChecks earlier in the pipeline, so that it is not affected by ElimByName's questionable
   type manipulations.
  • Loading branch information
odersky committed Jan 20, 2022
1 parent b340d82 commit 9274cce
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 242 deletions.
12 changes: 7 additions & 5 deletions compiler/src/dotty/tools/dotc/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import typer.{TyperPhase, RefChecks}
import parsing.Parser
import Phases.Phase
import transform._
import dotty.tools.backend.jvm.{CollectSuperCalls, GenBCode}
import dotty.tools.backend
import dotty.tools.dotc.transform.localopt.StringInterpolatorOpt
import backend.jvm.{CollectSuperCalls, GenBCode}
import localopt.StringInterpolatorOpt

/** The central class of the dotc compiler. The job of a compiler is to create
* runs, which process given `phases` in a given `rootContext`.
Expand Down Expand Up @@ -68,15 +68,17 @@ class Compiler {
new BetaReduce, // Reduce closure applications
new InlineVals, // Check right hand-sides of an `inline val`s
new ExpandSAMs, // Expand single abstract method closures to anonymous classes
new ElimRepeated) :: // Rewrite vararg parameters and arguments
new ElimRepeated, // Rewrite vararg parameters and arguments
new RefChecks) :: // Various checks mostly related to abstract members and overriding
List(new init.Checker) :: // Check initialization of objects
List(new ProtectedAccessors, // Add accessors for protected members
List(new CrossVersionChecks, // Check issues related to deprecated and experimental
new ProtectedAccessors, // Add accessors for protected members
new ExtensionMethods, // Expand methods of value classes with extension methods
new UncacheGivenAliases, // Avoid caching RHS of simple parameterless given aliases
new ElimByName, // Map by-name parameters to functions
new HoistSuperArgs, // Hoist complex arguments of supercalls to enclosing scope
new ForwardDepChecks, // Check that there are no forward references to local vals
new SpecializeApplyMethods, // Adds specialized methods to FunctionN
new RefChecks, // Various checks mostly related to abstract members and overriding
new TryCatchPatterns, // Compile cases in try/catch
new PatternMatcher) :: // Compile pattern matches
List(new ElimOpaque, // Turn opaque into normal aliases
Expand Down
111 changes: 111 additions & 0 deletions compiler/src/dotty/tools/dotc/transform/ForwardDepChecks.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package dotty.tools
package dotc
package transform

import core.*
import Symbols.*, Types.*, Contexts.*, Flags.*, Decorators.*, reporting.*
import util.Spans.Span
import util.Store
import collection.immutable
import ast.tpd
import MegaPhase.MiniPhase

object ForwardDepChecks:

import tpd.*

val name: String = "forwardDepChecks"

type LevelAndIndex = immutable.Map[Symbol, (LevelInfo, Int)]

class OptLevelInfo {
def levelAndIndex: LevelAndIndex = Map()
def enterReference(sym: Symbol, span: Span): Unit = ()
}

/** A class to help in forward reference checking */
class LevelInfo(outerLevelAndIndex: LevelAndIndex, stats: List[Tree])(using Context)
extends OptLevelInfo {
override val levelAndIndex: LevelAndIndex =
stats.foldLeft(outerLevelAndIndex, 0) {(mi, stat) =>
val (m, idx) = mi
val m1 = stat match {
case stat: MemberDef => m.updated(stat.symbol, (this, idx))
case _ => m
}
(m1, idx + 1)
}._1
var maxIndex: Int = Int.MinValue
var refSpan: Span = _
var refSym: Symbol = _

override def enterReference(sym: Symbol, span: Span): Unit =
if (sym.exists && sym.owner.isTerm)
levelAndIndex.get(sym) match {
case Some((level, idx)) if (level.maxIndex < idx) =>
level.maxIndex = idx
level.refSpan = span
level.refSym = sym
case _ =>
}
}

val NoLevelInfo: OptLevelInfo = new OptLevelInfo()

class ForwardDepChecks extends MiniPhase:
import ForwardDepChecks.*
import tpd.*

override def phaseName: String = ForwardDepChecks.name

override def runsAfter: Set[String] = Set(ElimByName.name)

private var LevelInfo: Store.Location[OptLevelInfo] = _
private def currentLevel(using Context): OptLevelInfo = ctx.store(LevelInfo)

override def initContext(ctx: FreshContext): Unit =
LevelInfo = ctx.addLocation(NoLevelInfo)

override def prepareForStats(trees: List[Tree])(using Context): Context =
if (ctx.owner.isTerm)
ctx.fresh.updateStore(LevelInfo, new LevelInfo(currentLevel.levelAndIndex, trees))
else ctx

override def transformValDef(tree: ValDef)(using Context): ValDef =
val sym = tree.symbol
if sym.exists && sym.owner.isTerm && !sym.is(Lazy) then
currentLevel.levelAndIndex.get(sym) match
case Some((level, symIdx)) if symIdx <= level.maxIndex =>
report.error(ForwardReferenceExtendsOverDefinition(sym, level.refSym),
ctx.source.atSpan(level.refSpan))
case _ =>
tree

override def transformIdent(tree: Ident)(using Context): Ident = {
currentLevel.enterReference(tree.symbol, tree.span)
tree
}

override def transformApply(tree: Apply)(using Context): Apply = {
if (isSelfConstrCall(tree)) {
assert(currentLevel.isInstanceOf[LevelInfo], s"${ctx.owner}/" + i"$tree")
val level = currentLevel.asInstanceOf[LevelInfo]
if (level.maxIndex > 0) {
// An implementation restriction to avoid VerifyErrors and lazyvals mishaps; see SI-4717
report.debuglog("refsym = " + level.refSym)
report.error("forward reference not allowed from self constructor invocation",
ctx.source.atSpan(level.refSpan))
}
}
tree
}

override def transformNew(tree: New)(using Context): New = {
currentLevel.enterReference(tree.tpe.typeSymbol, tree.span)
tree.tpe.dealias.foreachPart {
case TermRef(_, s: Symbol) => currentLevel.enterReference(s, tree.span)
case _ =>
}
tree
}
end ForwardDepChecks
175 changes: 175 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/CrossVersionChecks.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package dotty.tools
package dotc
package transform

import core.*
import Symbols.*, Types.*, Contexts.*, Flags.*, SymUtils.*, Decorators.*, reporting.*
import util.SrcPos
import config.{ScalaVersion, NoScalaVersion, Feature}
import MegaPhase.MiniPhase
import scala.util.{Failure, Success}
import ast.tpd

class CrossVersionChecks extends MiniPhase:
import tpd.*

def phaseName = "crossVersionChecks"

override def runsAfterGroupsOf: Set[String] = Set(FirstTransform.name)
// We assume all type trees except TypeTree have been eliminated

// Note: if a symbol has both @deprecated and @migration annotations and both
// warnings are enabled, only the first one checked here will be emitted.
// I assume that's a consequence of some code trying to avoid noise by suppressing
// 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: SrcPos)(using Context): Unit =
checkDeprecated(sym, pos)
checkExperimental(sym, pos)

val xMigrationValue = ctx.settings.Xmigration.value
if xMigrationValue != NoScalaVersion then
checkMigration(sym, pos, xMigrationValue)
end checkUndesiredProperties

/** If @deprecated is present, and the point of reference is not enclosed
* in either a deprecated member or a scala bridge method, issue a warning.
*/
private def checkDeprecated(sym: Symbol, pos: SrcPos)(using Context): Unit =

/** is the owner an enum or its companion and also the owner of sym */
def isEnumOwner(owner: Symbol)(using Context) =
// pre: sym is an enumcase
if owner.isEnumClass then owner.companionClass eq sym.owner
else if owner.is(ModuleClass) && owner.companionClass.isEnumClass then owner eq sym.owner
else false

def isDeprecatedOrEnum(owner: Symbol)(using Context) =
// pre: sym is an enumcase
owner.isDeprecated
|| isEnumOwner(owner)

/**Scan the chain of outer declaring scopes from the current context
* a deprecation warning will be skipped if one the following holds
* for a given declaring scope:
* - the symbol associated with the scope is also deprecated.
* - if and only if `sym` is an enum case, the scope is either
* a module that declares `sym`, or the companion class of the
* module that declares `sym`.
*/
def skipWarning(using Context) =
ctx.owner.ownersIterator.exists(if sym.isEnumCase then isDeprecatedOrEnum else _.isDeprecated)

for annot <- sym.getAnnotation(defn.DeprecatedAnnot) do
if !skipWarning then
val msg = annot.argumentConstant(0).map(": " + _.stringValue).getOrElse("")
val since = annot.argumentConstant(1).map(" since " + _.stringValue).getOrElse("")
report.deprecationWarning(s"${sym.showLocated} is deprecated${since}${msg}", pos)

private def checkExperimental(sym: Symbol, pos: SrcPos)(using Context): Unit =
if sym.isExperimental && !ctx.owner.isInExperimentalScope then
Feature.checkExperimentalDef(sym, pos)

private def checkExperimentalSignature(sym: Symbol, pos: SrcPos)(using Context): Unit =
class Checker extends TypeTraverser:
def traverse(tp: Type): Unit =
if tp.typeSymbol.isExperimental then
Feature.checkExperimentalDef(tp.typeSymbol, pos)
else
traverseChildren(tp)
if !sym.isInExperimentalScope then
new Checker().traverse(sym.info)

private def checkExperimentalAnnots(sym: Symbol)(using Context): Unit =
if !sym.isInExperimentalScope then
for annot <- sym.annotations if annot.symbol.isExperimental do
Feature.checkExperimentalDef(annot.symbol, annot.tree)

/** If @migration is present (indicating that the symbol has changed semantics between versions),
* emit a warning.
*/
private def checkMigration(sym: Symbol, pos: SrcPos, xMigrationValue: ScalaVersion)(using Context): Unit =
for annot <- sym.getAnnotation(defn.MigrationAnnot) do
val migrationVersion = ScalaVersion.parse(annot.argumentConstant(1).get.stringValue)
migrationVersion match
case Success(symVersion) if xMigrationValue < symVersion =>
val msg = annot.argumentConstant(0).get.stringValue
report.warning(SymbolChangedSemanticsInVersion(sym, symVersion, msg), pos)
case Failure(ex) =>
report.warning(SymbolHasUnparsableVersionNumber(sym, ex.getMessage), pos)
case _ =>

/** Check that a deprecated val or def does not override a
* concrete, non-deprecated method. If it does, then
* deprecation is meaningless.
*/
private def checkDeprecatedOvers(tree: Tree)(using Context): Unit = {
val symbol = tree.symbol
if (symbol.isDeprecated) {
val concrOvers =
symbol.allOverriddenSymbols.filter(sym =>
!sym.isDeprecated && !sym.is(Deferred))
if (!concrOvers.isEmpty)
report.deprecationWarning(
symbol.toString + " overrides concrete, non-deprecated symbol(s):" +
concrOvers.map(_.name).mkString(" ", ", ", ""), tree.srcPos)
}
}

/** Check that classes extending experimental classes or nested in experimental classes have the @experimental annotation. */
private def checkExperimentalInheritance(cls: ClassSymbol)(using Context): Unit =
if !cls.isAnonymousClass && !cls.hasAnnotation(defn.ExperimentalAnnot) then
cls.info.parents.find(_.typeSymbol.isExperimental) match
case Some(parent) =>
report.error(em"extension of experimental ${parent.typeSymbol} must have @experimental annotation", cls.srcPos)
case _ =>
end checkExperimentalInheritance

override def transformValDef(tree: ValDef)(using Context): ValDef =
checkExperimentalAnnots(tree.symbol)
checkExperimentalSignature(tree.symbol, tree)
checkDeprecatedOvers(tree)
tree

override def transformDefDef(tree: DefDef)(using Context): DefDef =
checkExperimentalAnnots(tree.symbol)
checkExperimentalSignature(tree.symbol, tree)
checkDeprecatedOvers(tree)
tree

override def transformTemplate(tree: Template)(using Context): Tree =
val cls = ctx.owner.asClass
checkExperimentalInheritance(cls)
checkExperimentalAnnots(cls)
tree

override def transformIdent(tree: Ident)(using Context): Ident =
checkUndesiredProperties(tree.symbol, tree.srcPos)
tree

override def transformSelect(tree: Select)(using Context): Select =
checkUndesiredProperties(tree.symbol, tree.srcPos)
tree

override def transformNew(tree: New)(using Context): New =
checkUndesiredProperties(tree.tpe.typeSymbol, tree.srcPos)
tree

override def transformTypeTree(tree: TypeTree)(using Context): TypeTree =
val tpe = tree.tpe
tpe.foreachPart {
case TypeRef(_, sym: Symbol) =>
checkDeprecated(sym, tree.srcPos)
checkExperimental(sym, tree.srcPos)
case TermRef(_, sym: Symbol) =>
checkDeprecated(sym, tree.srcPos)
checkExperimental(sym, tree.srcPos)
case _ =>
}
tree

override def transformTypeDef(tree: TypeDef)(using Context): TypeDef =
checkExperimentalAnnots(tree.symbol)
tree

end CrossVersionChecks
Loading

0 comments on commit 9274cce

Please sign in to comment.