Skip to content

Get linting information from Typer instead of recreating its logic #18400

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion community-build/community-projects/izumi-reflect
Submodule izumi-reflect updated 130 files
2 changes: 1 addition & 1 deletion community-build/community-projects/specs2
Submodule specs2 updated 44 files
+0 −6 .git-blame-ignore-revs
+36 −14 .github/workflows/ci.yml
+1 −1 .scalafmt.conf
+5 −8 build.sbt
+10 −13 common/shared/src/main/scala/org/specs2/collection/Seqx.scala
+1 −1 common/shared/src/main/scala/org/specs2/control/Action.scala
+24 −36 common/shared/src/main/scala/org/specs2/execute/Result.scala
+1 −1 common/shared/src/main/scala/org/specs2/main/SystemProperties.scala
+1 −1 core/jvm/src/test/scala/org/specs2/control/ThrowablexSpec.scala
+1 −1 core/jvm/src/test/scala/org/specs2/reporter/TextPrinterSpec.scala
+1 −1 core/shared/src/main/scala/specs2/run.scala
+6 −3 core/shared/src/test/scala/org/specs2/matcher/TypecheckSpec.scala
+0 −38 examples/jvm/src/test/scala/examples/IntegrationSpec.scala
+0 −15 examples/jvm/src/test/scala/examples/integration/DatabaseIntegrationSpec.scala
+0 −15 examples/jvm/src/test/scala/examples/integration/RemoteServiceIntegrationSpec.scala
+0 −27 examples/jvm/src/test/scala/examples/integration/StartDatabase.scala
+2 −0 examples/src/test/scala/examples/DataTablesSpec.scala
+2 −0 examples/src/test/scala/examples/HelloWorldAutoExamplesSpec.scala
+2 −0 examples/src/test/scala/examples/HelloWorldSpec.scala
+2 −0 examples/src/test/scala/examples/HelloWorldUnitSpec.scala
+2 −0 examples/src/test/scala/examples/ScalaCheckExamplesSpec.scala
+2 −0 examples/src/test/scala/examples/SnippetsScriptSpec.scala
+2 −0 examples/src/test/scala/examples/StackSpec.scala
+1 −1 form/src/main/scala/org/specs2/form/Form.scala
+4 −2 guide/src/test/scala/org/specs2/Website.scala
+2 −2 guide/src/test/scala/org/specs2/guide/Specs2Variables.scala
+2 −5 guide/src/test/scala/org/specs2/guide/matchers/ExceptionMatchers.scala
+1 −1 junit/src/main/scala/org/specs2/reporter/JUnitXmlPrinter.scala
+1 −1 junit/src/main/scala/org/specs2/reporter/ShowDescription.scala
+1 −1 junit/src/main/scala/org/specs2/runner/Specs2TestEngine.scala
+1 −1 markdown/src/main/scala/org/specs2/text/Markdown.scala
+1 −1 matcher/shared/src/main/scala/org/specs2/matcher/EqualityMatcher.scala
+180 −101 matcher/shared/src/main/scala/org/specs2/matcher/ExceptionMatchers.scala
+3 −5 matcher/shared/src/main/scala/org/specs2/matcher/StringMatchers.scala
+4 −23 matcher/shared/src/main/scala/org/specs2/matcher/TryMatchers.scala
+1 −1 project/build.properties
+6 −6 project/depends.scala
+6 −6 project/plugins.sbt
+7 −7 scripts/wordle.scala
+1 −30 tests/jvm/src/test/scala/org/specs2/reporter/SbtPrinterSpec.scala
+49 −82 tests/shared/src/test/scala/org/specs2/matcher/ExceptionMatchersSpec.scala
+1 −1 tests/shared/src/test/scala/org/specs2/matcher/TraversableMatchersSpec.scala
+2 −2 tests/shared/src/test/scala/org/specs2/matcher/TryMatchersSpec.scala
+4 −3 tests/shared/src/test/scala/org/specs2/matcher/describe/DiffablePlusSpec.scala
2 changes: 1 addition & 1 deletion community-build/community-projects/stdLib213
Submodule stdLib213 updated 653 files
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/CompilationUnit.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
52 changes: 52 additions & 0 deletions compiler/src/dotty/tools/dotc/EventLog.scala
Original file line number Diff line number Diff line change
@@ -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

}
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Contexts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
96 changes: 41 additions & 55 deletions compiler/src/dotty/tools/dotc/transform/CheckUnused.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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


/**
Expand Down Expand Up @@ -55,13 +59,43 @@ 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
)
val fresh = ctx.fresh.setProperty(_key, data)
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 =
Expand Down Expand Up @@ -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
Expand All @@ -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 :
Expand All @@ -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 */
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
14 changes: 9 additions & 5 deletions compiler/src/dotty/tools/dotc/typer/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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` */
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/ImportInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion compiler/test/dotty/tools/vulpix/ParallelTesting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down