diff --git a/community-build/community-projects/izumi-reflect b/community-build/community-projects/izumi-reflect index c0756faa7311..540f08283069 160000 --- a/community-build/community-projects/izumi-reflect +++ b/community-build/community-projects/izumi-reflect @@ -1 +1 @@ -Subproject commit c0756faa7311f70c6da6af29b8cb25506634bf09 +Subproject commit 540f08283069aefd8a81fec1f3493c70217b6099 diff --git a/community-build/community-projects/munit b/community-build/community-projects/munit index 55ae3caecf69..92f3ad9e8261 160000 --- a/community-build/community-projects/munit +++ b/community-build/community-projects/munit @@ -1 +1 @@ -Subproject commit 55ae3caecf690c8f1a785d78798955c6042344ca +Subproject commit 92f3ad9e8261b4c142a551baaf61ef5fed84d36a diff --git a/community-build/community-projects/specs2 b/community-build/community-projects/specs2 index e42f7987b4ce..789f23b75db1 160000 --- a/community-build/community-projects/specs2 +++ b/community-build/community-projects/specs2 @@ -1 +1 @@ -Subproject commit e42f7987b4ce30d95fca3f30b9d508021f2fdac7 +Subproject commit 789f23b75db1cf7961d04468b21a2cc0d7ba32d8 diff --git a/community-build/community-projects/stdLib213 b/community-build/community-projects/stdLib213 index 6243e902928c..1a2521996bad 160000 --- a/community-build/community-projects/stdLib213 +++ b/community-build/community-projects/stdLib213 @@ -1 +1 @@ -Subproject commit 6243e902928c344fb0e82e21120bb257f08a2af2 +Subproject commit 1a2521996badfe4cb3d9b8cdecefacb1251faeb9 diff --git a/compiler/src/dotty/tools/dotc/CompilationUnit.scala b/compiler/src/dotty/tools/dotc/CompilationUnit.scala index a906d52ccd4e..d0776bae9719 100644 --- a/compiler/src/dotty/tools/dotc/CompilationUnit.scala +++ b/compiler/src/dotty/tools/dotc/CompilationUnit.scala @@ -28,6 +28,8 @@ class CompilationUnit protected (val source: SourceFile) { var tpdTree: tpd.Tree = tpd.EmptyTree + val eventLog: EventLog = new EventLog + /** Is this the compilation unit of a Java file */ def isJava: Boolean = source.file.name.endsWith(".java") diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index a6118732d4ae..ec2c6abe4835 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -35,7 +35,7 @@ class Compiler { protected def frontendPhases: List[List[Phase]] = List(new Parser) :: // Compiler frontend: scanner, parser List(new TyperPhase) :: // Compiler frontend: namer, typer - List(new CheckUnused.PostTyper) :: // Check for unused elements + List(new CheckUnused.AfterTyper) :: // Check for unused elements List(new YCheckPositions) :: // YCheck positions List(new sbt.ExtractDependencies) :: // Sends information on classes' dependencies to sbt via callbacks List(new semanticdb.ExtractSemanticDB) :: // Extract info into .semanticdb files @@ -50,7 +50,7 @@ class Compiler { List(new Pickler) :: // Generate TASTY info List(new Inlining) :: // Inline and execute macros List(new PostInlining) :: // Add mirror support for inlined code - List(new CheckUnused.PostInlining) :: // Check for unused elements + List(new CheckUnused.AfterInlining) :: // Check for unused elements List(new Staging) :: // Check staging levels and heal staged types List(new Splicing) :: // Replace level 1 splices with holes List(new PickleQuotes) :: // Turn quoted trees into explicit run-time data structures diff --git a/compiler/src/dotty/tools/dotc/EventLog.scala b/compiler/src/dotty/tools/dotc/EventLog.scala new file mode 100644 index 000000000000..f82a5f4cdaa9 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/EventLog.scala @@ -0,0 +1,52 @@ +package dotty.tools.dotc + +import dotty.tools.dotc.core.SymDenotations.NoDenotation.exists +import scala.collection.mutable +import core.* +import ast.* +import Symbols.* +import EventLog.* +import Contexts.* +import dotty.tools.dotc.transform.CheckUnused + +class EventLog { + private val entries: mutable.ListBuffer[Entry] = mutable.ListBuffer() + private var state = 0 + + inline def appendEntry(entry: Entry)(using Context): Unit = { + state match { + case Disabled => () + case Enabled => entries += entry + case Uninitialized => + val enabled = ctx.base.allPhases.exists { phase => + logReaderPhasesNames.contains(phase.phaseName) && phase.isRunnable + } + state = + if enabled then + entries += entry + Enabled + else Disabled + } + } + + def toSeq: Seq[Entry] = entries.toSeq + +} + +object EventLog { + + private val logReaderPhasesNames = Set( + CheckUnused.phaseNamePrefix + CheckUnused.afterTyperSuffix, + CheckUnused.phaseNamePrefix + CheckUnused.afterInliningSuffix + ) + + private val Uninitialized: Int = 0 + private val Enabled = 1 + private val Disabled = 2 + + sealed trait Entry + case class MatchedImportSelector(importSym: Symbol, selector: untpd.ImportSelector) extends Entry + case class ImplicitIntroducedViaImport(importSym: Symbol, tree: Tree, ref: TermRef) extends Entry + case object FindRefFinished extends Entry + +} diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 8a7f2ff4e051..0687f64cd9a2 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -210,7 +210,7 @@ object Contexts { private var implicitsCache: ContextualImplicits | Null = null def implicits: ContextualImplicits = { if (implicitsCache == null) - implicitsCache = { + implicitsCache = {d val implicitRefs: List[ImplicitRef] = if (isClassDefContext) try owner.thisType.implicitMembers @@ -357,6 +357,8 @@ object Contexts { final def isAfterTyper = base.isAfterTyper(phase) final def isTyper = base.isTyper(phase) + final def eventLog: EventLog = compilationUnit.eventLog + /** Is this a context for the members of a class definition? */ def isClassDefContext: Boolean = owner.isClass && (owner ne outer.owner) diff --git a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala index 0eeec0f3cbec..bc00795c2b42 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala @@ -25,7 +25,11 @@ import dotty.tools.dotc.core.Definitions import dotty.tools.dotc.core.NameKinds.WildcardParamName import dotty.tools.dotc.core.Symbols.Symbol import dotty.tools.dotc.core.StdNames.nme +import dotty.tools.dotc.EventLog import scala.math.Ordering +import scala.annotation.tailrec +import dotty.tools.dotc.util.Spans.Coord +import dotty.tools.dotc.util.Spans.Span /** @@ -55,6 +59,8 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke override def prepareForUnit(tree: tpd.Tree)(using Context): Context = val data = UnusedData() + data.usedImports = collectUsedImports + println("Used imports: " + data.usedImports) tree.getAttachment(_key).foreach(oldData => data.unusedAggregate = oldData.unusedAggregate ) @@ -62,6 +68,34 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke tree.putAttachment(_key, data) fresh + private def collectUsedImports(using Context): Map[Span, Set[ImportSelector]] = Map.empty + // @tailrec + // def collectUsedImportsFromEventLog( + // acc: Set[(Symbol, ImportSelector)], + // lastSelector: ImportSelector, + // eventLog: Seq[EventLog.Entry] + // ): Set[(Symbol, ImportSelector)] = + // eventLog match + // case EventLog.MatchedImportSelector(_, selector) :: tail => + // println("Selector") + // collectUsedImportsFromEventLog(acc, selector, tail) + // case EventLog.RefFoundInImport(importSym) :: tail => + // println("Adding") + // collectUsedImportsFromEventLog(acc + ((importSym, lastSelector)), lastSelector, tail) + // case Nil => + // acc + + // println("Log: " + ctx.eventLog.toSeq) + // val eventLog = ctx.eventLog.toSeq.dropWhile(!_.isInstanceOf[EventLog.MatchedImportSelector]) + // println("Reduced: " + eventLog) + // eventLog match + // case EventLog.MatchedImportSelector(_, selector) :: tail => + // collectUsedImportsFromEventLog(Set.empty, selector, tail) + // .groupBy(_._1.span).view.mapValues(_.map(_._2)).toMap + // case _ => Map.empty + + + // ========== END + REPORTING ========== override def transformUnit(tree: tpd.Tree)(using Context): tpd.Tree = @@ -305,6 +339,8 @@ end CheckUnused object CheckUnused: val phaseNamePrefix: String = "checkUnused" val description: String = "check for unused elements" + val afterTyperSuffix: String = "AfterTyper" + val afterInliningSuffix: String = "AfterInlining" enum PhaseMode: case Aggregate @@ -326,9 +362,9 @@ object CheckUnused: */ private val _key = Property.StickyKey[UnusedData] - class PostTyper extends CheckUnused(PhaseMode.Aggregate, "PostTyper", _key) + class AfterTyper extends CheckUnused(PhaseMode.Aggregate, afterTyperSuffix, _key) - class PostInlining extends CheckUnused(PhaseMode.Report, "PostInlining", _key) + class AfterInlining extends CheckUnused(PhaseMode.Report, afterInliningSuffix, _key) /** * A stateful class gathering the infos on : @@ -343,6 +379,7 @@ object CheckUnused: /** The current scope during the tree traversal */ val currScopeType: MutStack[ScopeType] = MutStack(ScopeType.Other) + var usedImports = Map[Span, Set[ImportSelector]]() var unusedAggregate: Option[UnusedResult] = None /* IMPORTS */ @@ -423,7 +460,8 @@ object CheckUnused: if !tpd.languageImport(imp.expr).nonEmpty && !imp.isGeneratedByEnum && !isTransparentAndInline(imp) then impInScope.top += imp unusedImport ++= imp.selectors.filter { s => - !shouldSelectorBeReported(imp, s) && !isImportExclusion(s) + !shouldSelectorBeReported(imp, s) && !isImportExclusion(s) && + usedImports.get(imp.expr.span).forall(!_.contains(s)) } /** Register (or not) some `val` or `def` according to the context, scope and flags */ @@ -463,36 +501,6 @@ object CheckUnused: def popScope()(using Context): Unit = // used symbol in this scope val used = usedInScope.pop().toSet - // used imports in this scope - val imports = impInScope.pop() - val kept = used.filterNot { (sym, isAccessible, optName, isDerived) => - // keep the symbol for outer scope, if it matches **no** import - // This is the first matching wildcard selector - var selWildCard: Option[ImportSelector] = None - - val matchedExplicitImport = imports.exists { imp => - sym.isInImport(imp, isAccessible, optName, isDerived) match - case None => false - case optSel@Some(sel) if sel.isWildcard => - if selWildCard.isEmpty then selWildCard = optSel - // We keep wildcard symbol for the end as they have the least precedence - false - case Some(sel) => - unusedImport -= sel - true - } - if !matchedExplicitImport && selWildCard.isDefined then - unusedImport -= selWildCard.get - true // a matching import exists so the symbol won't be kept for outer scope - else - matchedExplicitImport - } - - // if there's an outer scope - if usedInScope.nonEmpty then - // we keep the symbols not referencing an import in this scope - // as it can be the only reference to an outer import - usedInScope.top ++= kept // register usage in this scope for other warnings at the end of the phase usedDef ++= used.map(_._1) // retrieve previous scope type @@ -654,28 +662,6 @@ object CheckUnused: && c.owner.thisType.member(sym.name).alternatives.contains(sym) } - /** Given an import and accessibility, return selector that matches import<->symbol */ - private def isInImport(imp: tpd.Import, isAccessible: Boolean, symName: Option[Name], isDerived: Boolean)(using Context): Option[ImportSelector] = - val tpd.Import(qual, sels) = imp - val dealiasedSym = dealias(sym) - val simpleSelections = qual.tpe.member(sym.name).alternatives - val typeSelections = sels.flatMap(n => qual.tpe.member(n.name.toTypeName).alternatives) - val termSelections = sels.flatMap(n => qual.tpe.member(n.name.toTermName).alternatives) - val selectionsToDealias = typeSelections ::: termSelections - val qualHasSymbol = simpleSelections.map(_.symbol).contains(sym) || (simpleSelections ::: selectionsToDealias).map(_.symbol).map(dealias).contains(dealiasedSym) - def selector = sels.find(sel => (sel.name.toTermName == sym.name || sel.name.toTypeName == sym.name) && symName.map(n => n.toTermName == sel.rename).getOrElse(true)) - def dealiasedSelector = if(isDerived) sels.flatMap(sel => selectionsToDealias.map(m => (sel, m.symbol))).collect { - case (sel, sym) if dealias(sym) == dealiasedSym => sel - }.headOption else None - def givenSelector = if sym.is(Given) || sym.is(Implicit) - then sels.filter(sel => sel.isGiven && !sel.bound.isEmpty).find(sel => sel.boundTpe =:= sym.info) - else None - def wildcard = sels.find(sel => sel.isWildcard && ((sym.is(Given) == sel.isGiven && sel.bound.isEmpty) || sym.is(Implicit))) - if qualHasSymbol && (!isAccessible || sym.isRenamedSymbol(symName)) && sym.exists then - selector.orElse(dealiasedSelector).orElse(givenSelector).orElse(wildcard) // selector with name or wildcard (or given) - else - None - private def isRenamedSymbol(symNameInScope: Option[Name])(using Context) = sym.name != nme.NO_NAME && symNameInScope.exists(_.toSimpleName != sym.name.toSimpleName) diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index ac6cc701be87..7428768cc4e4 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -44,7 +44,7 @@ object Implicits: /** An implicit definition `implicitRef` that is visible under a different name, `alias`. * Gets generated if an implicit ref is imported via a renaming import. */ - class RenamedImplicitRef(val underlyingRef: TermRef, val alias: TermName) extends ImplicitRef { + class RenamedImplicitRef(val underlyingRef: TermRef, val alias: TermName, val fromImport: Option[ImportInfo] = None) extends ImplicitRef { def implicitName(using Context): TermName = alias } @@ -422,7 +422,7 @@ object Implicits: * @param isExtension Whether the result is an extension method application * @param tstate The typer state to be committed if this alternative is chosen */ - case class SearchSuccess(tree: Tree, ref: TermRef, level: Int, isExtension: Boolean = false)(val tstate: TyperState, val gstate: GadtConstraint) + case class SearchSuccess(tree: Tree, ref: TermRef, level: Int, isExtension: Boolean = false, fromCandidate: Option[Candidate] = None)(val tstate: TyperState, val gstate: GadtConstraint) extends SearchResult with RefAndLevel with Showable /** A failed search */ @@ -877,7 +877,7 @@ trait Implicits: val inferred = inferImplicit(adjust(to), from, from.span) inferred match { - case SearchSuccess(_, ref, _, false) if isOldStyleFunctionConversion(ref.underlying) => + case SearchSuccess(_, ref, _, false, _) if isOldStyleFunctionConversion(ref.underlying) => report.migrationWarning( em"The conversion ${ref} will not be applied implicitly here in Scala 3 because only implicit methods and instances of Conversion class will continue to work as implicit views.", from @@ -1207,7 +1207,7 @@ trait Implicits: ctx.reporter.removeBufferedMessages res else - SearchSuccess(adapted, ref, cand.level, cand.isExtension)(ctx.typerState, ctx.gadt) + SearchSuccess(adapted, ref, cand.level, cand.isExtension, Some(cand))(ctx.typerState, ctx.gadt) } /** An implicit search; parameters as in `inferImplicit` */ @@ -1586,11 +1586,15 @@ trait Implicits: // effectively in a more inner context than any other definition provided by // explicit definitions. Consequently these terms have the highest priority and no // other candidates need to be considered. - recursiveRef match + val result = recursiveRef match case ref: TermRef => SearchSuccess(tpd.ref(ref).withSpan(span.startPos), ref, 0)(ctx.typerState, ctx.gadt) case _ => searchImplicit(contextual = true) + result.fromCandidate.foreach { cand => + ctx.eventLog.appendLog(cand.) + } + result end bestImplicit def implicitScope(tp: Type): OfTypeImplicits = ctx.run.nn.implicitScope(tp) diff --git a/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala b/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala index ba05cba229ae..508eb312eab9 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala @@ -143,7 +143,7 @@ class ImportInfo(symf: Context ?=> Symbol, if isEligible && ref.denot.asSingleDenotation.matchesImportBound(bound) then ref :: Nil else Nil else if renamed == ref.name then ref :: Nil - else RenamedImplicitRef(ref, renamed) :: Nil + else RenamedImplicitRef(ref, renamed, Some(this)) :: Nil } else for @@ -153,7 +153,7 @@ class ImportInfo(symf: Context ?=> Symbol, val original = reverseMapping(renamed).nn val ref = TermRef(pre, original, denot) if renamed == original then ref - else RenamedImplicitRef(ref, renamed) + else RenamedImplicitRef(ref, renamed, Some(this)) /** The root import symbol hidden by this symbol, or NoSymbol if no such symbol is hidden. * Note: this computation needs to work even for un-initialized import infos, and diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index fc1b90ea356d..b7ec3ddc6887 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -52,6 +52,7 @@ import Nullables._ import NullOpsDecorator._ import cc.CheckCaptures import config.Config +import EventLog._ import scala.annotation.constructorOnly import dotty.tools.dotc.rewrites.Rewrites @@ -254,7 +255,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer * @param using_Context the outer context of `precCtx` */ def checkImportAlternatives(previous: Type, prevPrec: BindingPrec, prevCtx: Context)(using Context): Type = - def addAltImport(altImp: TermRef) = if !TypeComparer.isSameRef(previous, altImp) && !altImports.uncheckedNN.exists(TypeComparer.isSameRef(_, altImp)) @@ -327,6 +327,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val other = recur(selectors.tail) if other.exists && found.exists && found != other then fail(em"reference to `$name` is ambiguous; it is imported twice") + ctx.eventLog.appendEntry(MatchedImportSelector(imp.importSym, selector)) found if selector.rename == termName && !selector.isUnimport then @@ -518,7 +519,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer loop(NoContext) } - findRefRecur(NoType, BindingPrec.NothingBound, NoContext) + val result = findRefRecur(NoType, BindingPrec.NothingBound, NoContext) + ctx.eventLog.appendEntry(FindRefFinished) + result } /** If `tree`'s type is a `TermRef` identified by flow typing to be non-null, then diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala index bccbcbee29e1..4b1a375e7c7d 100644 --- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala +++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala @@ -291,7 +291,7 @@ trait ParallelTesting extends RunnerOrchestration { self => /** This callback is executed once the compilation of this test source finished */ private final def onComplete(testSource: TestSource, reportersOrCrash: Try[Seq[TestReporter]], logger: LoggedRunnable): Unit = reportersOrCrash match { - case TryFailure(exn) => onFailure(testSource, Nil, logger, Some(s"Fatal compiler crash when compiling: ${testSource.title}:\n${exn.getMessage}${exn.getStackTrace.map("\n\tat " + _).mkString}")) + case TryFailure(exn) => onFailure(testSource, Nil, logger, Some(s"Fatal compiler crash when compiling: ${testSource.title}:\n ${exn.toString()} ${exn.getMessage}${exn.getStackTrace.map("\n\tat " + _).mkString}")) case TrySuccess(reporters) if !reporters.exists(_.skipped) => maybeFailureMessage(testSource, reporters) match { case Some(msg) => onFailure(testSource, reporters, logger, Option(msg).filter(_.nonEmpty))