diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala
index ef65b699d06d..955597261327 100644
--- a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala
+++ b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala
@@ -818,7 +818,7 @@ trait BCodeSkelBuilder extends BCodeHelpers {
 
       methSymbol  = dd.symbol
       jMethodName = methSymbol.javaSimpleName
-      returnType  = asmMethodType(dd.symbol).returnType
+      returnType  = asmMethodType(methSymbol).returnType
       isMethSymStaticCtor = methSymbol.isStaticConstructor
 
       resetMethodBookkeeping(dd)
@@ -915,7 +915,7 @@ trait BCodeSkelBuilder extends BCodeHelpers {
             for (p <- params) { emitLocalVarScope(p.symbol, veryFirstProgramPoint, onePastLastProgramPoint, force = true) }
           }
 
-          if (isMethSymStaticCtor) { appendToStaticCtor(dd) }
+          if (isMethSymStaticCtor) { appendToStaticCtor() }
         } // end of emitNormalMethodBody()
 
         lineNumber(rhs)
@@ -936,7 +936,7 @@ trait BCodeSkelBuilder extends BCodeHelpers {
      *
      *  TODO document, explain interplay with `fabricateStaticInitAndroid()`
      */
-    private def appendToStaticCtor(dd: DefDef): Unit = {
+    private def appendToStaticCtor(): Unit = {
 
       def insertBefore(
             location: asm.tree.AbstractInsnNode,
diff --git a/compiler/src/dotty/tools/backend/jvm/ClassfileWriters.scala b/compiler/src/dotty/tools/backend/jvm/ClassfileWriters.scala
index 536ab887a740..5dcafbef750f 100644
--- a/compiler/src/dotty/tools/backend/jvm/ClassfileWriters.scala
+++ b/compiler/src/dotty/tools/backend/jvm/ClassfileWriters.scala
@@ -12,10 +12,10 @@ import java.util.zip.{CRC32, Deflater, ZipEntry, ZipOutputStream}
 
 import dotty.tools.dotc.core.Contexts.*
 import dotty.tools.dotc.core.Decorators.em
+import dotty.tools.dotc.util.chaining.*
 import dotty.tools.io.{AbstractFile, PlainFile, VirtualFile}
 import dotty.tools.io.PlainFile.toPlainFile
 import BTypes.InternalName
-import scala.util.chaining._
 import dotty.tools.io.JarArchive
 
 import scala.language.unsafeNulls
@@ -180,7 +180,7 @@ class ClassfileWriters(frontendAccess: PostProcessorFrontendAccess) {
       // important detail here, even on Windows, Zinc expects the separator within the jar
       // to be the system default, (even if in the actual jar file the entry always uses '/').
       // see https://github.com/sbt/zinc/blob/dcddc1f9cfe542d738582c43f4840e17c053ce81/internal/compiler-bridge/src/main/scala/xsbt/JarUtils.scala#L47
-      val pathInJar = 
+      val pathInJar =
         if File.separatorChar == '/' then relativePath
         else relativePath.replace('/', File.separatorChar)
       PlainFile.toPlainFile(Paths.get(s"${file.absolutePath}!$pathInJar"))
diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala
index b3757a9ddce7..c888dabdd61f 100644
--- a/compiler/src/dotty/tools/dotc/Compiler.scala
+++ b/compiler/src/dotty/tools/dotc/Compiler.scala
@@ -8,7 +8,6 @@ import cc.CheckCaptures
 import parsing.Parser
 import Phases.Phase
 import transform.*
-import dotty.tools.backend
 import backend.jvm.{CollectSuperCalls, GenBCode}
 import localopt.StringInterpolatorOpt
 
@@ -35,8 +34,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 CheckShadowing) :: // Check shadowing elements
+    List(CheckUnused.PostTyper(), CheckShadowing()) :: // Check for unused, shadowed elements
     List(new YCheckPositions) ::    // YCheck positions
     List(new sbt.ExtractDependencies) :: // Sends information on classes' dependencies to sbt via callbacks
     List(new semanticdb.ExtractSemanticDB.ExtractSemanticInfo) :: // Extract info into .semanticdb files
@@ -51,10 +49,10 @@ 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 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
+    List(new CheckUnused.PostInlining) ::  // Check for unused elements
     Nil
 
   /** Phases dealing with the transformation from pickled trees to backend trees */
diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala
index 9db12e21913a..ca180e5ea612 100644
--- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala
+++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala
@@ -45,6 +45,14 @@ object desugar {
    */
   val UntupledParam: Property.Key[Unit] = Property.StickyKey()
 
+  /** An attachment key to indicate that a ValDef originated from a pattern.
+   */
+  val PatternVar: Property.Key[Unit] = Property.StickyKey()
+
+  /** An attachment key for Trees originating in for-comprehension, such as tupling of assignments.
+   */
+  val ForArtifact: Property.Key[Unit] = Property.StickyKey()
+
   /** What static check should be applied to a Match? */
   enum MatchCheck {
     case None, Exhaustive, IrrefutablePatDef, IrrefutableGenFrom
@@ -1221,7 +1229,7 @@ object desugar {
       val matchExpr =
         if (tupleOptimizable) rhs
         else
-          val caseDef = CaseDef(pat, EmptyTree, makeTuple(ids))
+          val caseDef = CaseDef(pat, EmptyTree, makeTuple(ids).withAttachment(ForArtifact, ()))
           Match(makeSelector(rhs, MatchCheck.IrrefutablePatDef), caseDef :: Nil)
       vars match {
         case Nil if !mods.is(Lazy) =>
@@ -1251,6 +1259,7 @@ object desugar {
                   ValDef(named.name.asTermName, tpt, selector(n))
                     .withMods(mods)
                     .withSpan(named.span)
+                    .withAttachment(PatternVar, ())
                 )
           flatTree(firstDef :: restDefs)
       }
@@ -1542,6 +1551,7 @@ object desugar {
     val vdef = ValDef(named.name.asTermName, tpt, rhs)
       .withMods(mods)
       .withSpan(original.span.withPoint(named.span.start))
+      .withAttachment(PatternVar, ())
     val mayNeedSetter = valDef(vdef)
     mayNeedSetter
   }
@@ -1733,7 +1743,7 @@ object desugar {
               case _ => Modifiers()
             makePatDef(valeq, mods, defpat, rhs)
           }
-          val rhs1 = makeFor(nme.map, nme.flatMap, GenFrom(defpat0, gen.expr, gen.checkMode) :: Nil, Block(pdefs, makeTuple(id0 :: ids)))
+          val rhs1 = makeFor(nme.map, nme.flatMap, GenFrom(defpat0, gen.expr, gen.checkMode) :: Nil, Block(pdefs, makeTuple(id0 :: ids).withAttachment(ForArtifact, ())))
           val allpats = gen.pat :: pats
           val vfrom1 = GenFrom(makeTuple(allpats), rhs1, GenCheckMode.Ignore)
           makeFor(mapName, flatMapName, vfrom1 :: rest1, body)
diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala
index 7652dcfc9928..dc8c2add27be 100644
--- a/compiler/src/dotty/tools/dotc/ast/Trees.scala
+++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala
@@ -31,7 +31,7 @@ object Trees {
 
   /** Property key for backquoted identifiers and definitions */
   val Backquoted: Property.StickyKey[Unit] = Property.StickyKey()
-  
+
   val SyntheticUnit: Property.StickyKey[Unit] = Property.StickyKey()
 
   /** Trees take a parameter indicating what the type of their `tpe` field
@@ -765,6 +765,7 @@ object Trees {
     override def isEmpty: Boolean = !hasType
     override def toString: String =
       s"TypeTree${if (hasType) s"[$typeOpt]" else ""}"
+    def isInferred = false
   }
 
   /** Tree that replaces a level 1 splices in pickled (level 0) quotes.
@@ -787,6 +788,7 @@ object Trees {
    */
   class InferredTypeTree[+T <: Untyped](implicit @constructorOnly src: SourceFile) extends TypeTree[T]:
     type ThisTree[+T <: Untyped] <: InferredTypeTree[T]
+    override def isInferred = true
 
   /** ref.type */
   case class SingletonTypeTree[+T <: Untyped] private[ast] (ref: Tree[T])(implicit @constructorOnly src: SourceFile)
diff --git a/compiler/src/dotty/tools/dotc/config/CliCommand.scala b/compiler/src/dotty/tools/dotc/config/CliCommand.scala
index b76af885765c..5ec51c1b5b0f 100644
--- a/compiler/src/dotty/tools/dotc/config/CliCommand.scala
+++ b/compiler/src/dotty/tools/dotc/config/CliCommand.scala
@@ -7,7 +7,7 @@ import Settings.*
 import core.Contexts.*
 import printing.Highlighting
 
-import scala.util.chaining.given
+import dotty.tools.dotc.util.chaining.*
 import scala.PartialFunction.cond
 
 trait CliCommand:
diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
index 6928e0617069..8dfe72cf85d4 100644
--- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
+++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
@@ -12,7 +12,7 @@ import dotty.tools.io.{AbstractFile, Directory, JDK9Reflectors, PlainDirectory}
 import dotty.tools.backend.jvm.BackendUtils.classfileVersionMap
 import Setting.ChoiceWithHelp
 
-import scala.util.chaining.*
+import dotty.tools.dotc.util.chaining.*
 
 import java.util.zip.Deflater
 
@@ -179,28 +179,20 @@ private sealed trait WarningSettings:
     choices = List(
       ChoiceWithHelp("nowarn", ""),
       ChoiceWithHelp("all", ""),
-      ChoiceWithHelp(
-        name = "imports",
-        description = "Warn if an import selector is not referenced.\n" +
-        "NOTE : overrided by -Wunused:strict-no-implicit-warn"),
+      ChoiceWithHelp("imports", "Warn if an import selector is not referenced."),
       ChoiceWithHelp("privates", "Warn if a private member is unused"),
       ChoiceWithHelp("locals", "Warn if a local definition is unused"),
       ChoiceWithHelp("explicits", "Warn if an explicit parameter is unused"),
       ChoiceWithHelp("implicits", "Warn if an implicit parameter is unused"),
       ChoiceWithHelp("params", "Enable -Wunused:explicits,implicits"),
+      ChoiceWithHelp("patvars","Warn if a variable bound in a pattern is unused"),
+      //ChoiceWithHelp("inlined", "Apply -Wunused to inlined expansions"), // TODO
       ChoiceWithHelp("linted", "Enable -Wunused:imports,privates,locals,implicits"),
       ChoiceWithHelp(
         name = "strict-no-implicit-warn",
         description = "Same as -Wunused:import, only for imports of explicit named members.\n" +
         "NOTE : This overrides -Wunused:imports and NOT set by -Wunused:all"
       ),
-      // ChoiceWithHelp("patvars","Warn if a variable bound in a pattern is unused"),
-      ChoiceWithHelp(
-        name = "unsafe-warn-patvars",
-        description = "(UNSAFE) Warn if a variable bound in a pattern is unused.\n" +
-        "This warning can generate false positive, as warning cannot be\n" +
-        "suppressed yet."
-      )
     ),
     default = Nil
   )
@@ -212,7 +204,6 @@ private sealed trait WarningSettings:
     // Is any choice set for -Wunused?
     def any(using Context): Boolean = Wall.value || Wunused.value.nonEmpty
 
-    // overrided by strict-no-implicit-warn
     def imports(using Context) =
       (allOr("imports") || allOr("linted")) && !(strictNoImplicitWarn)
     def locals(using Context) =
@@ -226,9 +217,8 @@ private sealed trait WarningSettings:
     def params(using Context) = allOr("params")
     def privates(using Context) =
       allOr("privates") || allOr("linted")
-    def patvars(using Context) =
-      isChoiceSet("unsafe-warn-patvars") // not with "all"
-      // allOr("patvars") // todo : rename once fixed
+    def patvars(using Context) = allOr("patvars")
+    def inlined(using Context) = isChoiceSet("inlined")
     def linted(using Context) =
       allOr("linted")
     def strictNoImplicitWarn(using Context) =
diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala
index 4aed3d310b03..26ddc47c6b89 100644
--- a/compiler/src/dotty/tools/dotc/core/Definitions.scala
+++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala
@@ -482,6 +482,8 @@ class Definitions {
     @tu lazy val Predef_undefined: Symbol = ScalaPredefModule.requiredMethod(nme.???)
   @tu lazy val ScalaPredefModuleClass: ClassSymbol = ScalaPredefModule.moduleClass.asClass
 
+  @tu lazy val SameTypeClass: ClassSymbol = requiredClass("scala.=:=")
+  @tu lazy val SameType_refl: Symbol = SameTypeClass.companionModule.requiredMethod(nme.refl)
   @tu lazy val SubTypeClass: ClassSymbol = requiredClass("scala.<:<")
   @tu lazy val SubType_refl: Symbol = SubTypeClass.companionModule.requiredMethod(nme.refl)
 
@@ -816,6 +818,7 @@ class Definitions {
   @tu lazy val QuotedExprClass: ClassSymbol = requiredClass("scala.quoted.Expr")
 
   @tu lazy val QuotesClass: ClassSymbol = requiredClass("scala.quoted.Quotes")
+    @tu lazy val Quotes_reflectModule: Symbol = QuotesClass.requiredClass("reflectModule")
     @tu lazy val Quotes_reflect: Symbol = QuotesClass.requiredValue("reflect")
       @tu lazy val Quotes_reflect_asTerm: Symbol = Quotes_reflect.requiredMethod("asTerm")
       @tu lazy val Quotes_reflect_Apply: Symbol = Quotes_reflect.requiredValue("Apply")
@@ -941,6 +944,7 @@ class Definitions {
   def NonEmptyTupleClass(using Context): ClassSymbol = NonEmptyTupleTypeRef.symbol.asClass
     lazy val NonEmptyTuple_tail: Symbol = NonEmptyTupleClass.requiredMethod("tail")
   @tu lazy val PairClass: ClassSymbol = requiredClass("scala.*:")
+    @tu lazy val PairClass_unapply: Symbol = PairClass.companionModule.requiredMethod("unapply")
 
   @tu lazy val TupleXXLClass: ClassSymbol = requiredClass("scala.runtime.TupleXXL")
   def TupleXXLModule(using Context): Symbol = TupleXXLClass.companionModule
@@ -1031,6 +1035,7 @@ class Definitions {
   @tu lazy val UncheckedCapturesAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedCaptures")
   @tu lazy val VolatileAnnot: ClassSymbol = requiredClass("scala.volatile")
   @tu lazy val WithPureFunsAnnot: ClassSymbol = requiredClass("scala.annotation.internal.WithPureFuns")
+  @tu lazy val LanguageFeatureMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.languageFeature")
   @tu lazy val BeanGetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.beanGetter")
   @tu lazy val BeanSetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.beanSetter")
   @tu lazy val FieldMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.field")
diff --git a/compiler/src/dotty/tools/dotc/report.scala b/compiler/src/dotty/tools/dotc/report.scala
index 5517bd005077..7bc301184022 100644
--- a/compiler/src/dotty/tools/dotc/report.scala
+++ b/compiler/src/dotty/tools/dotc/report.scala
@@ -1,13 +1,11 @@
 package dotty.tools.dotc
 
-import reporting.*
-import Diagnostic.*
-import util.{SourcePosition, NoSourcePosition, SrcPos}
-import core.*
-import Contexts.*, Flags.*, Symbols.*, Decorators.*
-import config.SourceVersion
 import ast.*
-import config.Feature.sourceVersion
+import core.*, Contexts.*, Flags.*, Symbols.*, Decorators.*
+import config.Feature.sourceVersion, config.SourceVersion
+import reporting.*, Diagnostic.*
+import util.{SourcePosition, NoSourcePosition, SrcPos}
+
 import java.lang.System.currentTimeMillis
 
 object report:
@@ -54,6 +52,9 @@ object report:
     else issueWarning(new FeatureWarning(msg, pos.sourcePos))
   end featureWarning
 
+  def warning(msg: Message, pos: SrcPos, origin: String)(using Context): Unit =
+    issueWarning(LintWarning(msg, addInlineds(pos), origin))
+
   def warning(msg: Message, pos: SrcPos)(using Context): Unit =
     issueWarning(new Warning(msg, addInlineds(pos)))
 
diff --git a/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala b/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala
index 6a2d88f4e82f..20be33716831 100644
--- a/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala
+++ b/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala
@@ -8,9 +8,9 @@ import dotty.tools.dotc.config.Settings.Setting
 import dotty.tools.dotc.core.Contexts.*
 import dotty.tools.dotc.interfaces.Diagnostic.{ERROR, INFO, WARNING}
 import dotty.tools.dotc.util.SourcePosition
+import dotty.tools.dotc.util.chaining.*
 
 import java.util.{Collections, Optional, List => JList}
-import scala.util.chaining.*
 import core.Decorators.toMessage
 
 object Diagnostic:
@@ -36,6 +36,18 @@ object Diagnostic:
     pos: SourcePosition
   ) extends Error(msg, pos)
 
+  /** A Warning with an origin. The semantics of `origin` depend on the warning.
+   *  For example, an unused import warning has an origin that specifies the unused selector.
+   *  The origin of a deprecation is the deprecated element.
+   */
+  trait OriginWarning(val origin: String):
+    self: Warning =>
+
+  /** Lints are likely to be filtered. Provide extra axes for filtering by `-Wconf`.
+   */
+  class LintWarning(msg: Message, pos: SourcePosition, origin: String)
+  extends Warning(msg, pos), OriginWarning(origin)
+
   class Warning(
     msg: Message,
     pos: SourcePosition
@@ -73,13 +85,9 @@ object Diagnostic:
     def enablingOption(using Context): Setting[Boolean] = ctx.settings.unchecked
   }
 
-  class DeprecationWarning(
-    msg: Message,
-    pos: SourcePosition,
-    val origin: String
-  ) extends ConditionalWarning(msg, pos) {
+  class DeprecationWarning(msg: Message, pos: SourcePosition, origin: String)
+  extends ConditionalWarning(msg, pos), OriginWarning(origin):
     def enablingOption(using Context): Setting[Boolean] = ctx.settings.deprecation
-  }
 
   class MigrationWarning(
     msg: Message,
@@ -104,5 +112,5 @@ class Diagnostic(
   override def diagnosticRelatedInformation: JList[interfaces.DiagnosticRelatedInformation] =
     Collections.emptyList()
 
-  override def toString: String = s"$getClass at $pos: $message"
+  override def toString: String = s"$getClass at $pos L${pos.line+1}: $message"
 end Diagnostic
diff --git a/compiler/src/dotty/tools/dotc/reporting/WConf.scala b/compiler/src/dotty/tools/dotc/reporting/WConf.scala
index b1afc2ab5dba..fd5c7cc0dbeb 100644
--- a/compiler/src/dotty/tools/dotc/reporting/WConf.scala
+++ b/compiler/src/dotty/tools/dotc/reporting/WConf.scala
@@ -14,11 +14,13 @@ import scala.annotation.internal.sharable
 import scala.util.matching.Regex
 
 enum MessageFilter:
-  def matches(message: Diagnostic): Boolean = this match
+  def matches(message: Diagnostic): Boolean =
+    import Diagnostic.*
+    this match
     case Any => true
-    case Deprecated => message.isInstanceOf[Diagnostic.DeprecationWarning]
-    case Feature => message.isInstanceOf[Diagnostic.FeatureWarning]
-    case Unchecked => message.isInstanceOf[Diagnostic.UncheckedWarning]
+    case Deprecated => message.isInstanceOf[DeprecationWarning]
+    case Feature => message.isInstanceOf[FeatureWarning]
+    case Unchecked => message.isInstanceOf[UncheckedWarning]
     case MessageID(errorId) => message.msg.errorId == errorId
     case MessagePattern(pattern) =>
       val noHighlight = message.msg.message.replaceAll("\\e\\[[\\d;]*[^\\d;]","")
@@ -31,7 +33,7 @@ enum MessageFilter:
       pattern.findFirstIn(path).nonEmpty
     case Origin(pattern) =>
       message match
-      case message: Diagnostic.DeprecationWarning => pattern.findFirstIn(message.origin).nonEmpty
+      case message: OriginWarning => pattern.findFirstIn(message.origin).nonEmpty
       case _ => false
     case None => false
 
@@ -56,12 +58,12 @@ object WConf:
   private type Conf = (List[MessageFilter], Action)
 
   def parseAction(s: String): Either[List[String], Action] = s match
-    case "error" | "e"            => Right(Error)
-    case "warning" | "w"          => Right(Warning)
-    case "verbose" | "v"          => Right(Verbose)
-    case "info" | "i"             => Right(Info)
-    case "silent" | "s"           => Right(Silent)
-    case _                        => Left(List(s"unknown action: `$s`"))
+    case "error"   | "e" => Right(Error)
+    case "warning" | "w" => Right(Warning)
+    case "verbose" | "v" => Right(Verbose)
+    case "info"    | "i" => Right(Info)
+    case "silent"  | "s" => Right(Silent)
+    case _               => Left(List(s"unknown action: `$s`"))
 
   private def regex(s: String) =
     try Right(s.r)
diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala
index f9210845bd54..886ed3979ba5 100644
--- a/compiler/src/dotty/tools/dotc/reporting/messages.scala
+++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala
@@ -3155,22 +3155,25 @@ extends SyntaxMsg(VolatileOnValID):
   protected def msg(using Context): String = "values cannot be volatile"
   protected def explain(using Context): String = ""
 
-class UnusedSymbol(errorText: String)(using Context)
+
+class UnusedSymbol(errorText: String, val actions: List[CodeAction] = Nil)(using Context)
 extends Message(UnusedSymbolID) {
   def kind = MessageKind.UnusedSymbol
 
   override def msg(using Context) = errorText
   override def explain(using Context) = ""
-}
-
-object UnusedSymbol {
-    def imports(using Context): UnusedSymbol = new UnusedSymbol(i"unused import")
-    def localDefs(using Context): UnusedSymbol = new UnusedSymbol(i"unused local definition")
-    def explicitParams(using Context): UnusedSymbol = new UnusedSymbol(i"unused explicit parameter")
-    def implicitParams(using Context): UnusedSymbol = new UnusedSymbol(i"unused implicit parameter")
-    def privateMembers(using Context): UnusedSymbol = new UnusedSymbol(i"unused private member")
-    def patVars(using Context): UnusedSymbol = new UnusedSymbol(i"unused pattern variable")
-}
+  override def actions(using Context) = this.actions
+}
+
+object UnusedSymbol:
+  def imports(actions: List[CodeAction])(using Context): UnusedSymbol = UnusedSymbol(i"unused import", actions)
+  def localDefs(using Context): UnusedSymbol = UnusedSymbol(i"unused local definition")
+  def explicitParams(using Context): UnusedSymbol = UnusedSymbol(i"unused explicit parameter")
+  def implicitParams(using Context): UnusedSymbol = UnusedSymbol(i"unused implicit parameter")
+  def privateMembers(using Context): UnusedSymbol = UnusedSymbol(i"unused private member")
+  def patVars(using Context): UnusedSymbol = UnusedSymbol(i"unused pattern variable")
+  def unsetLocals(using Context): UnusedSymbol = UnusedSymbol(i"unset local variable, consider using an immutable val instead")
+  def unsetPrivates(using Context): UnusedSymbol = UnusedSymbol(i"unset private variable, consider using an immutable val instead")
 
 final class QuotedTypeMissing(tpe: Type)(using Context) extends StagingMessage(QuotedTypeMissingID):
 
diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala
index 73f69dd6f2ad..59697796b76d 100644
--- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala
+++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala
@@ -24,7 +24,7 @@ import java.io.PrintWriter
 
 import scala.collection.mutable
 import scala.util.hashing.MurmurHash3
-import scala.util.chaining.*
+import dotty.tools.dotc.util.chaining.*
 
 /** This phase sends a representation of the API of classes to sbt via callbacks.
  *
diff --git a/compiler/src/dotty/tools/dotc/semanticdb/TypeOps.scala b/compiler/src/dotty/tools/dotc/semanticdb/TypeOps.scala
index 4293ecd6ca43..2d98535657a2 100644
--- a/compiler/src/dotty/tools/dotc/semanticdb/TypeOps.scala
+++ b/compiler/src/dotty/tools/dotc/semanticdb/TypeOps.scala
@@ -9,13 +9,13 @@ import core.Annotations.Annotation
 import core.Flags
 import core.Names.Name
 import core.StdNames.tpnme
-import scala.util.chaining.scalaUtilChainingOps
 
 import collection.mutable
 
 import dotty.tools.dotc.{semanticdb => s}
 import Scala3.{FakeSymbol, SemanticSymbol, WildcardTypeSymbol, TypeParamRefSymbol, TermParamRefSymbol, RefinementSymbol}
 import dotty.tools.dotc.core.Names.Designator
+import dotty.tools.dotc.util.chaining.*
 
 class TypeOps:
   import SymbolScopeOps.*
diff --git a/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala b/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala
index a85cabdd5460..7ecdf79af984 100644
--- a/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala
+++ b/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala
@@ -49,26 +49,22 @@ class CheckShadowing extends MiniPhase:
 
   override def description: String = CheckShadowing.description
 
+  override def isEnabled(using Context): Boolean = ctx.settings.Xlint.value.nonEmpty
+
   override def isRunnable(using Context): Boolean =
-    super.isRunnable &&
-    ctx.settings.Xlint.value.nonEmpty &&
-    !ctx.isJava
+    super.isRunnable && ctx.settings.Xlint.value.nonEmpty && !ctx.isJava
 
-  // Setup before the traversal
   override def prepareForUnit(tree: tpd.Tree)(using Context): Context =
     val data = ShadowingData()
     val fresh = ctx.fresh.setProperty(_key, data)
     shadowingDataApply(sd => sd.registerRootImports())(using fresh)
 
-  // Reporting on traversal's end
   override def transformUnit(tree: tpd.Tree)(using Context): tpd.Tree =
     shadowingDataApply(sd =>
       reportShadowing(sd.getShadowingResult)
     )
     tree
 
-  // MiniPhase traversal :
-
   override def prepareForPackageDef(tree: tpd.PackageDef)(using Context): Context =
     shadowingDataApply(sd => sd.inNewScope())
     ctx
@@ -91,16 +87,16 @@ class CheckShadowing extends MiniPhase:
     )
 
   override def prepareForTypeDef(tree: tpd.TypeDef)(using Context): Context =
-    if tree.symbol.isAliasType then // if alias, the parent is the current symbol
-      nestedTypeTraverser(tree.symbol).traverse(tree.rhs)
-    if tree.symbol.is(Param) then // if param, the parent is up
-      val owner = tree.symbol.owner
+    val sym = tree.symbol
+    if sym.isAliasType then // if alias, the parent is the current symbol
+      nestedTypeTraverser(sym).traverse(tree.rhs)
+    if sym.is(Param) then // if param, the parent is up
+      val owner = sym.owner
       val parent = if (owner.isConstructor) then owner.owner else owner
       nestedTypeTraverser(parent).traverse(tree.rhs)(using ctx.outer)
-      shadowingDataApply(sd => sd.registerCandidate(parent, tree))
-    else
-      ctx
-
+      if isValidTypeParamOwner(sym.owner) then
+        shadowingDataApply(sd => sd.registerCandidate(parent, tree))
+    ctx
 
   override def transformPackageDef(tree: tpd.PackageDef)(using Context): tpd.Tree =
     shadowingDataApply(sd => sd.outOfScope())
@@ -115,13 +111,14 @@ class CheckShadowing extends MiniPhase:
     tree
 
   override def transformTypeDef(tree: tpd.TypeDef)(using Context): tpd.Tree =
-    if tree.symbol.is(Param) &&  isValidTypeParamOwner(tree.symbol.owner) then // Do not register for constructors the work is done for the Class owned equivalent TypeDef
+    // Do not register for constructors the work is done for the Class owned equivalent TypeDef
+    if tree.symbol.is(Param) && isValidTypeParamOwner(tree.symbol.owner) then
       shadowingDataApply(sd => sd.computeTypeParamShadowsFor(tree.symbol.owner)(using ctx.outer))
-    if tree.symbol.isAliasType then // No need to start outer here, because the TypeDef reached here it's already the parent
+    // No need to start outer here, because the TypeDef reached here it's already the parent
+    if tree.symbol.isAliasType then
       shadowingDataApply(sd => sd.computeTypeParamShadowsFor(tree.symbol)(using ctx))
     tree
 
-  // Helpers :
   private def isValidTypeParamOwner(owner: Symbol)(using Context): Boolean =
     !owner.isConstructor && !owner.is(Synthetic) && !owner.is(Exported)
 
@@ -141,7 +138,7 @@ class CheckShadowing extends MiniPhase:
 
     override def traverse(tree: tpd.Tree)(using Context): Unit =
       tree match
-        case t:tpd.TypeDef =>
+        case t: tpd.TypeDef =>
           val newCtx = shadowingDataApply(sd =>
             sd.registerCandidate(parent, t)
           )
@@ -157,7 +154,7 @@ class CheckShadowing extends MiniPhase:
 
     override def traverse(tree: tpd.Tree)(using Context): Unit =
       tree match
-        case t:tpd.Import =>
+        case t: tpd.Import =>
           val newCtx = shadowingDataApply(sd => sd.registerImport(t))
           traverseChildren(tree)(using newCtx)
         case _ =>
@@ -240,7 +237,7 @@ object CheckShadowing:
         val declarationScope = ctx.effectiveScope
         val res = declarationScope.lookup(symbol.name)
         res match
-          case s: Symbol if s.isType => Some(s)
+          case s: Symbol if s.isType && s != symbol => Some(s)
           case _ => lookForUnitShadowedType(symbol)(using ctx.outer)
 
     /** Register if the valDef is a private declaration that shadows an inherited field */
@@ -310,4 +307,3 @@ object CheckShadowing:
       case class ShadowResult(warnings: List[ShadowWarning])
 
 end CheckShadowing
-
diff --git a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala
index 6e626fc5dd9e..bb5878737083 100644
--- a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala
+++ b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala
@@ -1,883 +1,916 @@
 package dotty.tools.dotc.transform
 
-import scala.annotation.tailrec
-import scala.collection.mutable
-
-import dotty.tools.uncheckedNN
-import dotty.tools.dotc.ast.tpd
-import dotty.tools.dotc.core.Symbols.*
-import dotty.tools.dotc.ast.tpd.{Inlined, TreeTraverser}
-import dotty.tools.dotc.ast.untpd
-import dotty.tools.dotc.ast.untpd.ImportSelector
+import dotty.tools.dotc.ast.desugar.{ForArtifact, PatternVar}
+import dotty.tools.dotc.ast.tpd.*
+import dotty.tools.dotc.ast.untpd, untpd.ImportSelector
 import dotty.tools.dotc.config.ScalaSettings
 import dotty.tools.dotc.core.Contexts.*
-import dotty.tools.dotc.core.Decorators.{em, i}
-import dotty.tools.dotc.core.Denotations.SingleDenotation
 import dotty.tools.dotc.core.Flags.*
-import dotty.tools.dotc.core.Phases.Phase
-import dotty.tools.dotc.core.StdNames
+import dotty.tools.dotc.core.Names.{Name, SimpleName, DerivedName, TermName, termName}
+import dotty.tools.dotc.core.NameOps.{isAnonymousFunctionName, isReplWrapperName, isContextFunction}
+import dotty.tools.dotc.core.NameKinds.{ContextBoundParamName, ContextFunctionParamName, WildcardParamName}
+import dotty.tools.dotc.core.StdNames.nme
+import dotty.tools.dotc.core.Symbols.{ClassSymbol, NoSymbol, Symbol, defn, isDeprecated, requiredClass, requiredModule}
+import dotty.tools.dotc.core.Types.*
 import dotty.tools.dotc.report
-import dotty.tools.dotc.reporting.Message
-import dotty.tools.dotc.reporting.UnusedSymbol as UnusedSymbolMessage
-import dotty.tools.dotc.typer.ImportInfo
-import dotty.tools.dotc.util.{Property, SrcPos}
-import dotty.tools.dotc.core.Mode
-import dotty.tools.dotc.core.Types.{AnnotatedType, ConstantType, NoType, TermRef, Type, TypeTraverser}
-import dotty.tools.dotc.core.Flags.flagsString
-import dotty.tools.dotc.core.Flags
-import dotty.tools.dotc.core.Names.{Name, TermName}
-import dotty.tools.dotc.core.NameOps.isReplWrapperName
+import dotty.tools.dotc.reporting.{CodeAction, UnusedSymbol}
+import dotty.tools.dotc.rewrites.Rewrites
 import dotty.tools.dotc.transform.MegaPhase.MiniPhase
-import dotty.tools.dotc.core.Annotations
-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.util.Spans.Span
-import scala.math.Ordering
+import dotty.tools.dotc.typer.{ImportInfo, Typer}
+import dotty.tools.dotc.typer.Deriving.OriginalTypeClass
+import dotty.tools.dotc.util.{Property, Spans, SrcPos}, Spans.Span
+import dotty.tools.dotc.util.Chars.{isLineBreakChar, isWhitespace}
+import dotty.tools.dotc.util.chaining.*
 
+import java.util.IdentityHashMap
 
-/**
- * A compiler phase that checks for unused imports or definitions
- *
- * Basically, it gathers definition/imports and their usage. If a
- * definition/imports does not have any usage, then it is reported.
- */
-class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _key: Property.Key[CheckUnused.UnusedData]) extends MiniPhase:
-  import CheckUnused.*
-  import UnusedData.*
-
-  private inline def unusedDataApply[U](inline f: UnusedData => U)(using Context): Context =
-    ctx.property(_key) match
-      case Some(ud) => f(ud)
-      case None     => ()
-    ctx
+import scala.collection.mutable, mutable.{ArrayBuilder, ListBuffer, Stack}
 
-  override def phaseName: String = CheckUnused.phaseNamePrefix + suffix
+import CheckUnused.*
 
-  override def description: String = CheckUnused.description
+/** A compiler phase that checks for unused imports or definitions.
+ */
+class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPhase:
 
-  override def isRunnable(using Context): Boolean =
-    super.isRunnable &&
-    ctx.settings.WunusedHas.any &&
-    !ctx.isJava
+  override def phaseName: String = s"checkUnused$suffix"
 
-  // ========== SETUP ============
+  override def description: String = "check for unused elements"
 
-  override def prepareForUnit(tree: tpd.Tree)(using Context): Context =
-    val data = UnusedData()
-    tree.getAttachment(_key).foreach(oldData =>
-      data.unusedAggregate = oldData.unusedAggregate
-    )
-    val fresh = ctx.fresh.setProperty(_key, data)
-    tree.putAttachment(_key, data)
-    fresh
+  override def isEnabled(using Context): Boolean = ctx.settings.WunusedHas.any
 
-  // ========== END + REPORTING ==========
+  override def isRunnable(using Context): Boolean = super.isRunnable && ctx.settings.WunusedHas.any && !ctx.isJava
 
-  override def transformUnit(tree: tpd.Tree)(using Context): tpd.Tree =
-    unusedDataApply { ud =>
-      ud.finishAggregation()
-      if(phaseMode == PhaseMode.Report) then
-        ud.unusedAggregate.foreach(reportUnused)
-    }
+  override def prepareForUnit(tree: Tree)(using Context): Context =
+    val infos = tree.getAttachment(refInfosKey).getOrElse:
+      RefInfos().tap(tree.withAttachment(refInfosKey, _))
+    ctx.fresh.setProperty(refInfosKey, infos)
+  override def transformUnit(tree: Tree)(using Context): tree.type =
+    if phaseMode == PhaseMode.Report then
+      reportUnused()
+      tree.removeAttachment(refInfosKey)
     tree
 
-  // ========== MiniPhase Prepare ==========
-  override def prepareForOther(tree: tpd.Tree)(using Context): Context =
-    // A standard tree traverser covers cases not handled by the Mega/MiniPhase
-    traverser.traverse(tree)
-    ctx
-
-  override def prepareForInlined(tree: tpd.Inlined)(using Context): Context =
-    traverser.traverse(tree.call)
-    ctx
-
-  override def prepareForIdent(tree: tpd.Ident)(using Context): Context =
+  override def transformIdent(tree: Ident)(using Context): tree.type =
     if tree.symbol.exists then
-      unusedDataApply { ud =>
-        @tailrec
-        def loopOnNormalizedPrefixes(prefix: Type, depth: Int): Unit =
-          // limit to 10 as failsafe for the odd case where there is an infinite cycle
-          if depth < 10 && prefix.exists then
-            ud.registerUsed(prefix.classSymbol, None)
-            loopOnNormalizedPrefixes(prefix.normalizedPrefix, depth + 1)
-
-        loopOnNormalizedPrefixes(tree.typeOpt.normalizedPrefix, depth = 0)
-        ud.registerUsed(tree.symbol, Some(tree.name))
-      }
+      // if in an inline expansion, resolve at summonInline (synthetic pos) or in an enclosing call site
+      val resolving =
+           refInfos.inlined.isEmpty
+        || tree.srcPos.isZeroExtentSynthetic
+        || refInfos.inlined.exists(_.sourcePos.contains(tree.srcPos.sourcePos))
+      if resolving && !ignoreTree(tree) then
+        resolveUsage(tree.symbol, tree.name, tree.typeOpt.importPrefix.skipPackageObject)
     else if tree.hasType then
-      unusedDataApply(_.registerUsed(tree.tpe.classSymbol, Some(tree.name)))
+      resolveUsage(tree.tpe.classSymbol, tree.name, tree.tpe.importPrefix.skipPackageObject)
+    tree
+
+  // import x.y; y may be rewritten x.y, also import x.z as y
+  override def transformSelect(tree: Select)(using Context): tree.type =
+    val name = tree.removeAttachment(OriginalName).getOrElse(nme.NO_NAME)
+    if tree.span.isSynthetic && tree.symbol == defn.TypeTest_unapply then
+      tree.qualifier.tpe.underlying.finalResultType match
+      case AppliedType(_, args) => // tycon.typeSymbol == defn.TypeTestClass
+        val res = args(1) // T in TypeTest[-S, T]
+        val target = res.dealias.typeSymbol
+        resolveUsage(target, target.name, res.importPrefix.skipPackageObject) // case _: T =>
+      case _ =>
+    else if tree.qualifier.span.isSynthetic || name.exists(_ != tree.symbol.name) then
+      if !ignoreTree(tree) then
+        resolveUsage(tree.symbol, name, tree.qualifier.tpe)
     else
-      ctx
-
-  override def prepareForSelect(tree: tpd.Select)(using Context): Context =
-    val name = tree.removeAttachment(OriginalName)
-    unusedDataApply(_.registerUsed(tree.symbol, name, includeForImport = tree.qualifier.span.isSynthetic))
-
-  override def prepareForBlock(tree: tpd.Block)(using Context): Context =
-    pushInBlockTemplatePackageDef(tree)
-
-  override def prepareForTemplate(tree: tpd.Template)(using Context): Context =
-    pushInBlockTemplatePackageDef(tree)
-
-  override def prepareForPackageDef(tree: tpd.PackageDef)(using Context): Context =
-    pushInBlockTemplatePackageDef(tree)
-
-  override def prepareForValDef(tree: tpd.ValDef)(using Context): Context =
-    unusedDataApply{ud =>
-      // do not register the ValDef generated for `object`
-      traverseAnnotations(tree.symbol)
-      if !tree.symbol.is(Module) then
-        ud.registerDef(tree)
-      if tree.name.startsWith("derived$") && tree.typeOpt != NoType then
-        ud.registerUsed(tree.typeOpt.typeSymbol, None, isDerived = true)
-      ud.addIgnoredUsage(tree.symbol)
-    }
-
-  override def prepareForDefDef(tree: tpd.DefDef)(using Context): Context =
-    unusedDataApply: ud =>
-      if !tree.symbol.is(Private) then
-        tree.termParamss.flatten.foreach { p =>
-          ud.addIgnoredParam(p.symbol)
-        }
-      ud.registerTrivial(tree)
-      traverseAnnotations(tree.symbol)
-      ud.registerDef(tree)
-      ud.addIgnoredUsage(tree.symbol)
-
-  override def prepareForTypeDef(tree: tpd.TypeDef)(using Context): Context =
-    unusedDataApply: ud =>
-      traverseAnnotations(tree.symbol)
-      if !tree.symbol.is(Param) then // Ignore type parameter (as Scala 2)
-        ud.registerDef(tree)
-        ud.addIgnoredUsage(tree.symbol)
-
-  override def prepareForBind(tree: tpd.Bind)(using Context): Context =
-    traverseAnnotations(tree.symbol)
-    unusedDataApply(_.registerPatVar(tree))
+      refUsage(tree.symbol)
+    tree
 
-  override def prepareForTypeTree(tree: tpd.TypeTree)(using Context): Context =
-    if !tree.isInstanceOf[tpd.InferredTypeTree] then typeTraverser(unusedDataApply).traverse(tree.tpe)
-    ctx
+  override def transformLiteral(tree: Literal)(using Context): tree.type =
+    tree.getAttachment(Typer.AdaptedTree).foreach(transformAllDeep)
+    tree
 
-  override def prepareForAssign(tree: tpd.Assign)(using Context): Context =
-    unusedDataApply{ ud =>
-      val sym = tree.lhs.symbol
-      if sym.exists then
-        ud.registerSetVar(sym)
-    }
+  override def prepareForCaseDef(tree: CaseDef)(using Context): Context =
+    nowarner.traverse(tree.pat)
+    ctx
 
-  // ========== MiniPhase Transform ==========
+  override def prepareForApply(tree: Apply)(using Context): Context =
+    // ignore tupling of for assignments, as they are not usages of vars
+    if tree.hasAttachment(ForArtifact) then
+      tree match
+      case Apply(TypeApply(Select(fun, nme.apply), _), args) =>
+        if fun.symbol.is(Module) && defn.isTupleClass(fun.symbol.companionClass) then
+          args.foreach(_.withAttachment(ForArtifact, ()))
+      case _ =>
+    ctx
 
-  override def transformBlock(tree: tpd.Block)(using Context): tpd.Tree =
-    popOutBlockTemplatePackageDef()
+  override def prepareForAssign(tree: Assign)(using Context): Context =
+    tree.lhs.putAttachment(Ignore, ()) // don't take LHS reference as a read
+    ctx
+  override def transformAssign(tree: Assign)(using Context): tree.type =
+    tree.lhs.removeAttachment(Ignore)
+    val sym = tree.lhs.symbol
+    if sym.exists then
+      refInfos.asss.addOne(sym)
     tree
 
-  override def transformTemplate(tree: tpd.Template)(using Context): tpd.Tree =
-    popOutBlockTemplatePackageDef()
+  override def prepareForMatch(tree: Match)(using Context): Context =
+    // exonerate case.pat against tree.selector (simple var pat only for now)
+    tree.selector match
+    case Ident(nm) => tree.cases.foreach(k => allowVariableBindings(List(nm), List(k.pat)))
+    case _ =>
+    ctx
+  override def transformMatch(tree: Match)(using Context): tree.type =
+    if tree.isInstanceOf[InlineMatch] && tree.selector.isEmpty then
+      val sf = defn.Compiletime_summonFrom
+      resolveUsage(sf, sf.name, NoPrefix)
     tree
 
-  override def transformPackageDef(tree: tpd.PackageDef)(using Context): tpd.Tree =
-    popOutBlockTemplatePackageDef()
+  override def transformTypeTree(tree: TypeTree)(using Context): tree.type =
+    tree.tpe match
+    case AnnotatedType(_, annot) => transformAllDeep(annot.tree)
+    case tpt if !tree.isInferred && tpt.typeSymbol.exists => resolveUsage(tpt.typeSymbol, tpt.typeSymbol.name, NoPrefix)
+    case _ =>
     tree
 
-  override def transformValDef(tree: tpd.ValDef)(using Context): tpd.Tree =
-    unusedDataApply(_.removeIgnoredUsage(tree.symbol))
+  override def prepareForInlined(tree: Inlined)(using Context): Context =
+    refInfos.inlined.push(tree.call.srcPos)
+    ctx
+  override def transformInlined(tree: Inlined)(using Context): tree.type =
+    val _ = refInfos.inlined.pop()
+    if !tree.call.isEmpty && phaseMode.eq(PhaseMode.Aggregate) then
+      transformAllDeep(tree.call)
     tree
 
-  override def transformDefDef(tree: tpd.DefDef)(using Context): tpd.Tree =
-    unusedDataApply(_.removeIgnoredUsage(tree.symbol))
+  override def prepareForBind(tree: Bind)(using Context): Context =
+    refInfos.register(tree)
+    ctx
+
+  override def prepareForValDef(tree: ValDef)(using Context): Context =
+    if !tree.symbol.is(Deferred) && tree.rhs.symbol != defn.Predef_undefined then
+      refInfos.register(tree)
+    ctx
+  override def transformValDef(tree: ValDef)(using Context): tree.type =
+    traverseAnnotations(tree.symbol)
+    if tree.name.startsWith("derived$") && tree.hasType then
+      def loop(t: Tree): Unit = t match
+        case Ident(name)  =>
+          val target =
+            val ts0 = t.tpe.typeSymbol
+            if ts0.is(ModuleClass) then ts0.companionModule else ts0
+          resolveUsage(target, name, t.tpe.underlyingPrefix.skipPackageObject)
+        case Select(t, _) => loop(t)
+        case _            =>
+      tree.getAttachment(OriginalTypeClass).foreach(loop)
     tree
 
-  override def transformTypeDef(tree: tpd.TypeDef)(using Context): tpd.Tree =
-    unusedDataApply(_.removeIgnoredUsage(tree.symbol))
+  override def prepareForDefDef(tree: DefDef)(using Context): Context =
+    def trivial = tree.symbol.is(Deferred) || isUnconsuming(tree.rhs)
+    def nontrivial = tree.symbol.isConstructor || tree.symbol.isAnonymousFunction
+    if !nontrivial && trivial then refInfos.skip.addOne(tree.symbol)
+    if tree.symbol.is(Inline) then
+      refInfos.inliners += 1
+    else if !tree.symbol.is(Deferred) && tree.rhs.symbol != defn.Predef_undefined then
+      refInfos.register(tree)
+    ctx
+  override def transformDefDef(tree: DefDef)(using Context): tree.type =
+    traverseAnnotations(tree.symbol)
+    if tree.symbol.is(Inline) then
+      refInfos.inliners -= 1
     tree
 
+  override def transformTypeDef(tree: TypeDef)(using Context): tree.type =
+    traverseAnnotations(tree.symbol)
+    if !tree.symbol.is(Param) then // type parameter to do?
+      refInfos.register(tree)
+    tree
 
-  // ---------- MiniPhase HELPERS -----------
+  override def prepareForTemplate(tree: Template)(using Context): Context =
+    ctx.fresh.setProperty(resolvedKey, Resolved())
+
+  override def prepareForPackageDef(tree: PackageDef)(using Context): Context =
+    ctx.fresh.setProperty(resolvedKey, Resolved())
+
+  override def prepareForStats(trees: List[Tree])(using Context): Context =
+    ctx.fresh.setProperty(resolvedKey, Resolved())
+
+  override def transformOther(tree: Tree)(using Context): tree.type =
+    tree match
+    case imp: Import =>
+      if phaseMode eq PhaseMode.Aggregate then
+        refInfos.register(imp)
+      transformAllDeep(imp.expr)
+      for selector <- imp.selectors do
+        if selector.isGiven then
+          selector.bound match
+          case untpd.TypedSplice(bound) => transformAllDeep(bound)
+          case _ =>
+    case AppliedTypeTree(tpt, args) =>
+      transformAllDeep(tpt)
+      args.foreach(transformAllDeep)
+    case RefinedTypeTree(tpt, refinements) =>
+      transformAllDeep(tpt)
+      refinements.foreach(transformAllDeep)
+    case LambdaTypeTree(tparams, body) =>
+      tparams.foreach(transformAllDeep)
+      transformAllDeep(body)
+    case SingletonTypeTree(ref) =>
+      // selftype of object is not a usage
+      val moduleSelfRef = ctx.owner.is(Module) && ctx.owner == tree.symbol.companionModule.moduleClass
+      if !moduleSelfRef then
+        transformAllDeep(ref)
+    case TypeBoundsTree(lo, hi, alias) =>
+      transformAllDeep(lo)
+      transformAllDeep(hi)
+      transformAllDeep(alias)
+    case tree: NamedArg => transformAllDeep(tree.arg)
+    case Annotated(arg, annot) =>
+      transformAllDeep(arg)
+      transformAllDeep(annot)
+    case Quote(body, tags) =>
+      transformAllDeep(body)
+      tags.foreach(transformAllDeep)
+    case Splice(expr) =>
+      transformAllDeep(expr)
+    case pat @ SplicePattern(body, args) =>
+      transformAllDeep(body)
+      args.foreach(transformAllDeep)
+    case MatchTypeTree(bound, selector, cases) =>
+      transformAllDeep(bound)
+      transformAllDeep(selector)
+      cases.foreach(transformAllDeep)
+    case ByNameTypeTree(result) =>
+      transformAllDeep(result)
+    //case _: InferredTypeTree => // do nothing
+    //case _: Export => // nothing to do
+    //case _ if tree.isType =>
+    case _ =>
+    tree
 
-  private def pushInBlockTemplatePackageDef(tree: tpd.Block | tpd.Template | tpd.PackageDef)(using Context): Context =
-    unusedDataApply { ud =>
-      ud.pushScope(UnusedData.ScopeType.fromTree(tree))
-    }
-    ctx
+  private def traverseAnnotations(sym: Symbol)(using Context): Unit =
+    for annot <- sym.denot.annotations do
+      transformAllDeep(annot.tree)
 
-  private def popOutBlockTemplatePackageDef()(using Context): Context =
-    unusedDataApply { ud =>
-      ud.popScope()
-    }
-    ctx
+  // if sym is not an enclosing element, record the reference
+  def refUsage(sym: Symbol)(using Context): Unit =
+    if !ctx.outersIterator.exists(cur => cur.owner eq sym) then
+      refInfos.refs.addOne(sym)
 
-  /**
-   * This traverse is the **main** component of this phase
+  /** Look up a reference in enclosing contexts to determine whether it was introduced by a definition or import.
+   *  The binding of highest precedence must then be correct.
    *
-   * It traverses the tree and gathers the data in the
-   * corresponding context property
+   *  Unqualified locals and fully qualified globals are neither imported nor in scope;
+   *  e.g., in `scala.Int`, `scala` is in scope for typer, but here we reverse-engineer the attribution.
+   *  For Select, lint does not look up `<empty>.scala` (so top-level syms look like magic) but records `scala.Int`.
+   *  For Ident, look-up finds the root import as usual. A competing import is OK because higher precedence.
    */
-  private def traverser = new TreeTraverser:
-    import tpd.*
-    import UnusedData.ScopeType
-
-    /* Register every imports, definition and usage */
-    override def traverse(tree: tpd.Tree)(using Context): Unit =
-      val newCtx = if tree.symbol.exists then ctx.withOwner(tree.symbol) else ctx
-      tree match
-        case imp: tpd.Import =>
-          unusedDataApply(_.registerImport(imp))
-          imp.selectors.filter(_.isGiven).map(_.bound).collect {
-            case untpd.TypedSplice(tree1) => tree1
-          }.foreach(traverse(_)(using newCtx))
-          traverseChildren(tree)(using newCtx)
-        case ident: Ident =>
-          prepareForIdent(ident)
-          traverseChildren(tree)(using newCtx)
-        case sel: Select =>
-          prepareForSelect(sel)
-          traverseChildren(tree)(using newCtx)
-        case tree: (tpd.Block | tpd.Template | tpd.PackageDef) =>
-          //! DIFFERS FROM MINIPHASE
-          pushInBlockTemplatePackageDef(tree)
-          traverseChildren(tree)(using newCtx)
-          popOutBlockTemplatePackageDef()
-        case t: tpd.ValDef =>
-          prepareForValDef(t)
-          traverseChildren(tree)(using newCtx)
-          transformValDef(t)
-        case t: tpd.DefDef =>
-          prepareForDefDef(t)
-          traverseChildren(tree)(using newCtx)
-          transformDefDef(t)
-        case t: tpd.TypeDef =>
-          prepareForTypeDef(t)
-          traverseChildren(tree)(using newCtx)
-          transformTypeDef(t)
-        case t: tpd.Bind =>
-          prepareForBind(t)
-          traverseChildren(tree)(using newCtx)
-        case t:tpd.Assign =>
-          prepareForAssign(t)
-          traverseChildren(tree)
-        case _: tpd.InferredTypeTree =>
-        case t@tpd.RefinedTypeTree(tpt, refinements) =>
-          //! DIFFERS FROM MINIPHASE
-          typeTraverser(unusedDataApply).traverse(t.tpe)
-          traverse(tpt)(using newCtx)
-        case t@tpd.TypeTree() =>
-          //! DIFFERS FROM MINIPHASE
-          typeTraverser(unusedDataApply).traverse(t.tpe)
-          traverseChildren(tree)(using newCtx)
-        case _ =>
-          //! DIFFERS FROM MINIPHASE
-          traverseChildren(tree)(using newCtx)
-    end traverse
-  end traverser
-
-  /** This is a type traverser which catch some special Types not traversed by the term traverser above */
-  private def typeTraverser(dt: (UnusedData => Any) => Unit)(using Context) = new TypeTraverser:
-    override def traverse(tp: Type): Unit =
-      if tp.typeSymbol.exists then dt(_.registerUsed(tp.typeSymbol, Some(tp.typeSymbol.name)))
-      tp match
-        case AnnotatedType(_, annot) =>
-          dt(_.registerUsed(annot.symbol, None))
-          traverseChildren(tp)
-        case _ =>
-          traverseChildren(tp)
-
-  /** This traverse the annotations of the symbol */
-  private def traverseAnnotations(sym: Symbol)(using Context): Unit =
-    sym.denot.annotations.foreach(annot => traverser.traverse(annot.tree))
-
-
-  /** Do the actual reporting given the result of the anaylsis */
-  private def reportUnused(res: UnusedData.UnusedResult)(using Context): Unit =
-    res.warnings.toList.sortBy(_.pos.span.point)(using Ordering[Int]).foreach { s =>
-      s match
-        case UnusedSymbol(t, _, WarnTypes.Imports) =>
-          report.warning(UnusedSymbolMessage.imports, t)
-        case UnusedSymbol(t, _, WarnTypes.LocalDefs) =>
-          report.warning(UnusedSymbolMessage.localDefs, t)
-        case UnusedSymbol(t, _, WarnTypes.ExplicitParams) =>
-          report.warning(UnusedSymbolMessage.explicitParams, t)
-        case UnusedSymbol(t, _, WarnTypes.ImplicitParams) =>
-          report.warning(UnusedSymbolMessage.implicitParams, t)
-        case UnusedSymbol(t, _, WarnTypes.PrivateMembers) =>
-          report.warning(UnusedSymbolMessage.privateMembers, t)
-        case UnusedSymbol(t, _, WarnTypes.PatVars) =>
-          report.warning(UnusedSymbolMessage.patVars, t)
-        case UnusedSymbol(t, _, WarnTypes.UnsetLocals) =>
-          report.warning("unset local variable, consider using an immutable val instead", t)
-        case UnusedSymbol(t, _, WarnTypes.UnsetPrivates) =>
-          report.warning("unset private variable, consider using an immutable val instead", t)
-    }
-
+  def resolveUsage(sym: Symbol, name: Name, prefix: Type)(using Context): Unit =
+    import PrecedenceLevels.*
+
+    def matchingSelector(info: ImportInfo): ImportSelector | Null =
+      val qtpe = info.site
+      def hasAltMember(nm: Name) = qtpe.member(nm).hasAltWith(_.symbol == sym)
+      def loop(sels: List[ImportSelector]): ImportSelector | Null = sels match
+        case sel :: sels =>
+          val matches =
+            if sel.isWildcard then
+              // the qualifier must have the target symbol as a member
+              hasAltMember(sym.name) && {
+                if sel.isGiven then // Further check that the symbol is a given or implicit and conforms to the bound
+                     sym.isOneOf(GivenOrImplicit)
+                  && (sel.bound.isEmpty || sym.info.finalResultType <:< sel.boundTpe)
+                  && (prefix.eq(NoPrefix) || qtpe =:= prefix)
+                else
+                  !sym.is(Given) // Normal wildcard, check that the symbol is not a given (but can be implicit)
+              }
+            else
+              // if there is an explicit name, it must match
+                 !name.exists(_.toTermName != sel.rename)
+              && (prefix.eq(NoPrefix) || qtpe =:= prefix)
+              && (hasAltMember(sel.name) || hasAltMember(sel.name.toTypeName))
+          if matches then sel else loop(sels)
+        case nil => null
+      loop(info.selectors)
+
+    def checkMember(ctxsym: Symbol): Boolean =
+      ctxsym.isClass && sym.owner.isClass
+      && ctxsym.thisType.baseClasses.contains(sym.owner)
+      && ctxsym.thisType.member(sym.name).hasAltWith(d => d.containsSym(sym) && !name.exists(_ != d.name))
+
+    // Attempt to cache a result at the given context. Not all contexts bear a cache, including NoContext.
+    // If there is already any result for the name and prefix, do nothing.
+    def addCached(where: Context, result: Precedence): Unit =
+      if where.moreProperties ne null then
+        where.property(resolvedKey) match
+        case Some(resolved) =>
+          resolved.record(sym, name, prefix, result)
+        case none =>
+
+    // Avoid spurious NoSymbol and also primary ctors which are never warned about.
+    if !sym.exists || sym.isPrimaryConstructor then return
+
+    // Find the innermost, highest precedence. Contexts have no nesting levels but assume correctness.
+    // If the sym is an enclosing definition (the owner of a context), it does not count toward usages.
+    val isLocal = sym.isLocalToBlock
+    var candidate: Context = NoContext
+    var cachePoint: Context = NoContext // last context with Resolved cache
+    var importer: ImportSelector | Null = null // non-null for import context
+    var precedence = NoPrecedence // of current resolution
+    var done = false
+    var cached = false
+    val ctxs = ctx.outersIterator
+    while !done && ctxs.hasNext do
+      val cur = ctxs.next()
+      if cur.owner eq sym then
+        addCached(cachePoint, Definition)
+        return // found enclosing definition
+      else if isLocal then
+        if cur.owner eq sym.owner then
+          done = true // for local def, just checking that it is not enclosing
+      else
+        val cachedPrecedence =
+          cur.property(resolvedKey) match
+          case Some(resolved) =>
+            // conservative, cache must be nested below the result context
+            if precedence.isNone then
+              cachePoint = cur // no result yet, and future result could be cached here
+            resolved.hasRecord(sym, name, prefix)
+          case none => NoPrecedence
+        cached = !cachedPrecedence.isNone
+        if cached then
+          // if prefer cached precedence, then discard previous result
+          if precedence.weakerThan(cachedPrecedence) then
+            candidate = NoContext
+            importer = null
+            cachePoint = cur // actual cache context
+            precedence = cachedPrecedence // actual cached precedence
+          done = true
+        else if cur.isImportContext then
+          val sel = matchingSelector(cur.importInfo.nn)
+          if sel != null then
+            if cur.importInfo.nn.isRootImport then
+              if precedence.weakerThan(OtherUnit) then
+                precedence = OtherUnit
+                candidate = cur
+                importer = sel
+              done = true
+            else if sel.isWildcard then
+              if precedence.weakerThan(Wildcard) then
+                precedence = Wildcard
+                candidate = cur
+                importer = sel
+            else
+              if precedence.weakerThan(NamedImport) then
+                precedence = NamedImport
+                candidate = cur
+                importer = sel
+        else if checkMember(cur.owner) then
+          if sym.srcPos.sourcePos.source == ctx.source then
+            precedence = Definition
+            candidate = cur
+            importer = null // ignore import in same scope; we can't check nesting level
+            done = true
+          else if precedence.weakerThan(OtherUnit) then
+            precedence = OtherUnit
+            candidate = cur
+    end while
+    // record usage and possibly an import
+    refInfos.refs.addOne(sym)
+    if candidate != NoContext && candidate.isImportContext && importer != null then
+      refInfos.sels.put(importer, ())
+    // possibly record that we have performed this look-up
+    // if no result was found, take it as Definition (local or rooted head of fully qualified path)
+    val adjusted = if precedence.isNone then Definition else precedence
+    if !cached && (cachePoint ne NoContext) then
+      addCached(cachePoint, adjusted)
+    if cachePoint ne ctx then
+      addCached(ctx, adjusted) // at this ctx, since cachePoint may be far up the outer chain
+  end resolveUsage
 end CheckUnused
 
 object CheckUnused:
-  val phaseNamePrefix: String = "checkUnused"
-  val description: String = "check for unused elements"
 
   enum PhaseMode:
     case Aggregate
     case Report
 
-  private enum WarnTypes:
-    case Imports
-    case LocalDefs
-    case ExplicitParams
-    case ImplicitParams
-    case PrivateMembers
-    case PatVars
-    case UnsetLocals
-    case UnsetPrivates
-
-  /**
-   * The key used to retrieve the "unused entity" analysis metadata,
-   * from the compilation `Context`
-   */
-  private val _key = Property.StickyKey[UnusedData]
+  val refInfosKey = Property.StickyKey[RefInfos]
 
-  val OriginalName = Property.StickyKey[Name]
+  val resolvedKey = Property.Key[Resolved]
 
-  class PostTyper extends CheckUnused(PhaseMode.Aggregate, "PostTyper", _key)
+  inline def refInfos(using Context): RefInfos = ctx.property(refInfosKey).get
 
-  class PostInlining extends CheckUnused(PhaseMode.Report, "PostInlining", _key)
+  inline def resolved(using Context): Resolved =
+    ctx.property(resolvedKey) match
+    case Some(res) => res
+    case _ => throw new MatchError("no Resolved for context")
 
-  /**
-   * A stateful class gathering the infos on :
-   * - imports
-   * - definitions
-   * - usage
-   */
-  private class UnusedData:
-    import collection.mutable.{Set => MutSet, Map => MutMap, Stack => MutStack, ListBuffer => MutList}
-    import UnusedData.*
-
-    /** The current scope during the tree traversal */
-    val currScopeType: MutStack[ScopeType] = MutStack(ScopeType.Other)
-
-    var unusedAggregate: Option[UnusedResult] = None
-
-    /* IMPORTS */
-    private val impInScope = MutStack(MutList[ImportSelectorData]())
-    /**
-     * We store the symbol along with their accessibility without import.
-     * Accessibility to their definition in outer context/scope
-     *
-     * See the `isAccessibleAsIdent` extension method below in the file
-     */
-    private val usedInScope = MutStack(MutSet[(Symbol, Option[Name], Boolean)]())
-    private val usedInPosition = MutMap.empty[Name, MutSet[Symbol]]
-    /* unused import collected during traversal */
-    private val unusedImport = MutList.empty[ImportSelectorData]
-
-    /* LOCAL DEF OR VAL / Private Def or Val / Pattern variables */
-    private val localDefInScope = MutList.empty[tpd.MemberDef]
-    private val privateDefInScope = MutList.empty[tpd.MemberDef]
-    private val explicitParamInScope = MutList.empty[tpd.MemberDef]
-    private val implicitParamInScope = MutList.empty[tpd.MemberDef]
-    private val patVarsInScope = MutList.empty[tpd.Bind]
-
-    /** All variables sets*/
-    private val setVars = MutSet[Symbol]()
-
-    /** All used symbols */
-    private val usedDef = MutSet[Symbol]()
-    /** Do not register as used */
-    private val doNotRegister = MutSet[Symbol]()
-
-    /** Trivial definitions, avoid registering params */
-    private val trivialDefs = MutSet[Symbol]()
-
-    private val paramsToSkip = MutSet[Symbol]()
-
-
-    def finishAggregation(using Context)(): Unit =
-      val unusedInThisStage = this.getUnused
-      this.unusedAggregate match {
-        case None =>
-          this.unusedAggregate = Some(unusedInThisStage)
-        case Some(prevUnused) =>
-          val intersection = unusedInThisStage.warnings.intersect(prevUnused.warnings)
-          this.unusedAggregate = Some(UnusedResult(intersection))
-      }
+  /** Attachment holding the name of an Ident as written by the user. */
+  val OriginalName = Property.StickyKey[Name]
 
+  /** Suppress warning in a tree, such as a patvar name allowed by special convention. */
+  val NoWarn = Property.StickyKey[Unit]
 
-    /**
-     * Register a found (used) symbol along with its name
-     *
-     * The optional name will be used to target the right import
-     * as the same element can be imported with different renaming
-     */
-    def registerUsed(sym: Symbol, name: Option[Name], includeForImport: Boolean = true, isDerived: Boolean = false)(using Context): Unit =
-      if sym.exists && !isConstructorOfSynth(sym) && !doNotRegister(sym) then
-        if sym.isConstructor then
-          registerUsed(sym.owner, None, includeForImport) // constructor are "implicitly" imported with the class
-        else
-          // If the symbol is accessible in this scope without an import, do not register it for unused import analysis
-          val includeForImport1 =
-            includeForImport
-              && (name.exists(_.toTermName != sym.name.toTermName) || !sym.isAccessibleAsIdent)
-
-          def addIfExists(sym: Symbol): Unit =
-            if sym.exists then
-              usedDef += sym
-              if includeForImport1 then
-                usedInScope.top += ((sym, name, isDerived))
-          addIfExists(sym)
-          addIfExists(sym.companionModule)
-          addIfExists(sym.companionClass)
-          if sym.sourcePos.exists then
-            for n <- name do
-              usedInPosition.getOrElseUpdate(n, MutSet.empty) += sym
-
-    /** Register a symbol that should be ignored */
-    def addIgnoredUsage(sym: Symbol)(using Context): Unit =
-      doNotRegister ++= sym.everySymbol
-
-    /** Remove a symbol that shouldn't be ignored anymore */
-    def removeIgnoredUsage(sym: Symbol)(using Context): Unit =
-      doNotRegister --= sym.everySymbol
-
-    def addIgnoredParam(sym: Symbol)(using Context): Unit =
-      paramsToSkip += sym
-
-    /** Register an import */
-    def registerImport(imp: tpd.Import)(using Context): Unit =
-      if
-        !tpd.languageImport(imp.expr).nonEmpty
-          && !imp.isGeneratedByEnum
-          && !isTransparentAndInline(imp)
-          && currScopeType.top != ScopeType.ReplWrapper // #18383 Do not report top-level import's in the repl as unused
-      then
-        val qualTpe = imp.expr.tpe
-
-        // Put wildcard imports at the end, because they have lower priority within one Import
-        val reorderdSelectors =
-          val (wildcardSels, nonWildcardSels) = imp.selectors.partition(_.isWildcard)
-          nonWildcardSels ::: wildcardSels
-
-        val excludedMembers: mutable.Set[TermName] = mutable.Set.empty
-
-        val newDataInScope =
-          for sel <- reorderdSelectors yield
-            val data = new ImportSelectorData(qualTpe, sel)
-            if shouldSelectorBeReported(imp, sel) || isImportExclusion(sel) || isImportIgnored(imp, sel) then
-              // Immediately mark the selector as used
-              data.markUsed()
-              if isImportExclusion(sel) then
-                excludedMembers += sel.name
-            if sel.isWildcard && excludedMembers.nonEmpty then
-              // mark excluded members for the wildcard import
-              data.markExcluded(excludedMembers.toSet)
-            data
-        impInScope.top.appendAll(newDataInScope)
-    end registerImport
-
-    /** Register (or not) some `val` or `def` according to the context, scope and flags */
-    def registerDef(memDef: tpd.MemberDef)(using Context): Unit =
-      if memDef.isValidMemberDef && !isDefIgnored(memDef) then
-        if memDef.isValidParam then
-          if memDef.symbol.isOneOf(GivenOrImplicit) then
-            if !paramsToSkip.contains(memDef.symbol) then
-              implicitParamInScope += memDef
-          else if !paramsToSkip.contains(memDef.symbol) then
-            explicitParamInScope += memDef
-        else if currScopeType.top == ScopeType.Local then
-          localDefInScope += memDef
-        else if memDef.shouldReportPrivateDef then
-          privateDefInScope += memDef
-
-    /** Register pattern variable */
-    def registerPatVar(patvar: tpd.Bind)(using Context): Unit =
-      if !patvar.symbol.isUnusedAnnot then
-        patVarsInScope += patvar
-
-    /** enter a new scope */
-    def pushScope(newScopeType: ScopeType): Unit =
-      // unused imports :
-      currScopeType.push(newScopeType)
-      impInScope.push(MutList())
-      usedInScope.push(MutSet())
-
-    def registerSetVar(sym: Symbol): Unit =
-      setVars += sym
-
-    /**
-     * leave the current scope and do :
-     *
-     * - If there are imports in this scope check for unused ones
-     */
-    def popScope()(using Context): Unit =
-      currScopeType.pop()
-      val usedInfos = usedInScope.pop()
-      val selDatas = impInScope.pop()
-
-      for usedInfo <- usedInfos do
-        val (sym, optName, isDerived) = usedInfo
-        val usedData = selDatas.find { selData =>
-          sym.isInImport(selData, optName, isDerived)
-        }
-        usedData match
-          case Some(data) =>
-            data.markUsed()
-          case None =>
-            // Propagate the symbol one level up
-            if usedInScope.nonEmpty then
-              usedInScope.top += usedInfo
-      end for // each in `used`
-
-      for selData <- selDatas do
-        if !selData.isUsed then
-          unusedImport += selData
-    end popScope
-
-    /**
-     * Leave the scope and return a `List` of unused `ImportSelector`s
-     *
-     * The given `List` is sorted by line and then column of the position
-     */
+  /** Ignore reference. */
+  val Ignore = Property.StickyKey[Unit]
 
-    def getUnused(using Context): UnusedResult =
-      popScope()
+  class PostTyper extends CheckUnused(PhaseMode.Aggregate, "PostTyper")
 
-      def isUsedInPosition(name: Name, span: Span): Boolean =
-        usedInPosition.get(name) match
-          case Some(syms) => syms.exists(sym => span.contains(sym.span))
-          case None       => false
+  class PostInlining extends CheckUnused(PhaseMode.Report, "PostInlining")
 
-      val sortedImp =
-        if ctx.settings.WunusedHas.imports || ctx.settings.WunusedHas.strictNoImplicitWarn then
-          unusedImport.toList
-            .map(d => UnusedSymbol(d.selector.srcPos, d.selector.name, WarnTypes.Imports))
-        else
-          Nil
-      // Partition to extract unset local variables from usedLocalDefs
-      val (usedLocalDefs, unusedLocalDefs) =
-        if ctx.settings.WunusedHas.locals then
-          localDefInScope.toList.partition(d => d.symbol.usedDefContains)
-        else
-          (Nil, Nil)
-      val sortedLocalDefs =
-        unusedLocalDefs
-          .filterNot(d => isUsedInPosition(d.symbol.name, d.span))
-          .filterNot(d => containsSyntheticSuffix(d.symbol))
-          .map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.LocalDefs))
-      val unsetLocalDefs = usedLocalDefs.filter(isUnsetVarDef).map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.UnsetLocals)).toList
-
-      val sortedExplicitParams =
-        if ctx.settings.WunusedHas.explicits then
-          explicitParamInScope.toList
-            .filterNot(d => d.symbol.usedDefContains)
-            .filterNot(d => isUsedInPosition(d.symbol.name, d.span))
-            .filterNot(d => containsSyntheticSuffix(d.symbol))
-            .map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.ExplicitParams))
-        else
-          Nil
-      val sortedImplicitParams =
-        if ctx.settings.WunusedHas.implicits then
-          implicitParamInScope.toList
-            .filterNot(d => d.symbol.usedDefContains)
-            .filterNot(d => containsSyntheticSuffix(d.symbol))
-            .map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.ImplicitParams))
-        else
-          Nil
-      // Partition to extract unset private variables from usedPrivates
-      val (usedPrivates, unusedPrivates) =
-        if ctx.settings.WunusedHas.privates then
-          privateDefInScope.toList.partition(d => d.symbol.usedDefContains)
+  class RefInfos:
+    val defs = mutable.Set.empty[(Symbol, SrcPos)]    // definitions
+    val pats = mutable.Set.empty[(Symbol, SrcPos)]    // pattern variables
+    val refs = mutable.Set.empty[Symbol]              // references
+    val asss = mutable.Set.empty[Symbol]              // targets of assignment
+    val skip = mutable.Set.empty[Symbol]              // methods to skip (don't warn about their params)
+    val imps = new IdentityHashMap[Import, Unit]         // imports
+    val sels = new IdentityHashMap[ImportSelector, Unit] // matched selectors
+    def register(tree: Tree)(using Context): Unit = if inlined.isEmpty then
+      tree match
+      case imp: Import =>
+        if inliners == 0
+          && languageImport(imp.expr).isEmpty
+          && !imp.isGeneratedByEnum
+          && !ctx.outer.owner.name.isReplWrapperName
+        then
+          imps.put(imp, ())
+      case tree: Bind =>
+        if !tree.name.isInstanceOf[DerivedName] && !tree.name.is(WildcardParamName) && !tree.hasAttachment(NoWarn) then
+          pats.addOne((tree.symbol, tree.namePos))
+      case tree: ValDef if tree.hasAttachment(PatternVar) =>
+        if !tree.name.isInstanceOf[DerivedName] then
+          pats.addOne((tree.symbol, tree.namePos))
+      case tree: NamedDefTree =>
+        if (tree.symbol ne NoSymbol) && !tree.name.isWildcard then
+          defs.addOne((tree.symbol, tree.namePos))
+      case _ =>
+        //println(s"OTHER ${tree.symbol}")
+        if tree.symbol ne NoSymbol then
+          defs.addOne((tree.symbol, tree.srcPos))
+
+    val inlined = Stack.empty[SrcPos] // enclosing call.srcPos of inlined code (expansions)
+    var inliners = 0 // depth of inline def (not inlined yet)
+  end RefInfos
+
+  // Symbols already resolved in the given Context (with name and prefix of lookup).
+  class Resolved:
+    import PrecedenceLevels.*
+    private val seen = mutable.Map.empty[Symbol, List[(Name, Type, Precedence)]].withDefaultValue(Nil)
+    // if a result has been recorded, return it; otherwise, NoPrecedence.
+    def hasRecord(symbol: Symbol, name: Name, prefix: Type)(using Context): Precedence =
+      seen(symbol).find((n, p, _) => n == name && p =:= prefix) match
+      case Some((_, _, r)) => r
+      case none => NoPrecedence
+    // "record" the look-up result, if there is not already a result for the name and prefix.
+    def record(symbol: Symbol, name: Name, prefix: Type, result: Precedence)(using Context): Unit =
+      require(NoPrecedence.weakerThan(result))
+      seen.updateWith(symbol):
+        case svs @ Some(vs) =>
+          if vs.exists((n, p, _) => n == name && p =:= prefix) then svs
+          else Some((name, prefix, result) :: vs)
+        case none => Some((name, prefix, result) :: Nil)
+
+  // Names are resolved by definitions and imports, which have four precedence levels:
+  object PrecedenceLevels:
+    opaque type Precedence = Int
+    inline def NoPrecedence: Precedence = 5
+    inline def OtherUnit: Precedence = 4   // root import or def from another compilation unit via enclosing package
+    inline def Wildcard: Precedence = 3    // wildcard import
+    inline def NamedImport: Precedence = 2 // specific import
+    inline def Definition: Precedence = 1  // def from this compilation unit
+    extension (p: Precedence)
+      inline def weakerThan(q: Precedence): Boolean = p > q
+      inline def isNone: Boolean = p == NoPrecedence
+
+  def reportUnused()(using Context): Unit =
+    for (msg, pos, origin) <- warnings do
+      if origin.isEmpty then report.warning(msg, pos)
+      else report.warning(msg, pos, origin)
+      msg.actions.headOption.foreach(Rewrites.applyAction)
+
+  type MessageInfo = (UnusedSymbol, SrcPos, String) // string is origin or empty
+
+  def warnings(using Context): Array[MessageInfo] =
+    val actionable = ctx.settings.rewrite.value.nonEmpty
+    val warnings = ArrayBuilder.make[MessageInfo]
+    def warnAt(pos: SrcPos)(msg: UnusedSymbol, origin: String = ""): Unit = warnings.addOne((msg, pos, origin))
+    val infos = refInfos
+
+    def checkUnassigned(sym: Symbol, pos: SrcPos) =
+      if sym.isLocalToBlock then
+        if ctx.settings.WunusedHas.locals && sym.is(Mutable) && !infos.asss(sym) then
+          warnAt(pos)(UnusedSymbol.unsetLocals)
+      else if ctx.settings.WunusedHas.privates && sym.isAllOf(Private | Mutable) && !infos.asss(sym) then
+        warnAt(pos)(UnusedSymbol.unsetPrivates)
+
+    def checkPrivate(sym: Symbol, pos: SrcPos) =
+      if ctx.settings.WunusedHas.privates
+        && !sym.isPrimaryConstructor
+        && sym.is(Private, butNot = SelfName | Synthetic | CaseAccessor)
+        && !sym.isSerializationSupport
+        && !(sym.is(Mutable) && sym.isSetter && sym.owner.is(Trait)) // tracks sym.underlyingSymbol sibling getter
+      then
+        warnAt(pos)(UnusedSymbol.privateMembers)
+
+    def checkParam(sym: Symbol, pos: SrcPos) =
+      val m = sym.owner
+      def allowed =
+        val dd = defn
+           m.isDeprecated
+        || m.is(Synthetic) && !m.isAnonymousFunction
+        || m.hasAnnotation(defn.UnusedAnnot) // param of unused method
+        || sym.info.isSingleton
+        || m.isConstructor && m.owner.thisType.baseClasses.contains(defn.AnnotationClass)
+      def checkExplicit(): Unit =
+        // A class param is unused if its param accessor is unused.
+        // (The class param is not assigned to a field until constructors.)
+        // A local param accessor warns as a param; a private accessor as a private member.
+        // Avoid warning for case class elements because they are aliased via unapply.
+        if m.isPrimaryConstructor then
+          val alias = m.owner.info.member(sym.name)
+          if alias.exists then
+            val aliasSym = alias.symbol
+            if aliasSym.isAllOf(PrivateParamAccessor, butNot = CaseAccessor) && !infos.refs(alias.symbol) then
+              if aliasSym.is(Local) then
+                if ctx.settings.WunusedHas.explicits then
+                  warnAt(pos)(UnusedSymbol.explicitParams)
+              else
+                if ctx.settings.WunusedHas.privates then
+                  warnAt(pos)(UnusedSymbol.privateMembers)
+        else if ctx.settings.WunusedHas.explicits
+          && !sym.is(Synthetic) // param to setter is unused bc there is no field yet
+          && !(sym.owner.is(ExtensionMethod) && {
+            m.paramSymss.dropWhile(_.exists(_.isTypeParam)) match
+            case (h :: Nil) :: Nil => h == sym // param is the extended receiver
+            case _ => false
+          })
+          && !sym.name.isInstanceOf[DerivedName]
+          && !ctx.platform.isMainMethod(m)
+        then
+          warnAt(pos)(UnusedSymbol.explicitParams)
+      end checkExplicit
+      // begin
+      if !infos.skip(m)
+        && !allowed
+      then
+        checkExplicit()
+    end checkParam
+
+    def checkImplicit(sym: Symbol, pos: SrcPos) =
+      val m = sym.owner
+      def allowed =
+        val dd = defn
+           m.isDeprecated
+        || m.is(Synthetic)
+        || sym.owner.name.isContextFunction    // a ubiquitous parameter
+        || sym.name.is(ContextBoundParamName) && sym.info.typeSymbol.isMarkerTrait // a ubiquitous parameter
+        || m.hasAnnotation(dd.UnusedAnnot)          // param of unused method
+        || sym.info.typeSymbol.match                // more ubiquity
+           case dd.DummyImplicitClass | dd.SubTypeClass | dd.SameTypeClass => true
+           case _ => false
+        || sym.info.isSingleton // DSL friendly
+        || sym.isCanEqual
+        || sym.info.typeSymbol.hasAnnotation(dd.LanguageFeatureMetaAnnot)
+        || sym.info.isInstanceOf[RefinedType] // can't be expressed as a context bound
+      if ctx.settings.WunusedHas.implicits
+        && !infos.skip(m)
+        && !m.isEffectivelyOverride
+        && !allowed
+      then
+        if m.isPrimaryConstructor then
+          val alias = m.owner.info.member(sym.name)
+          if alias.exists then
+            val aliasSym = alias.symbol
+            val checking =
+                 aliasSym.isAllOf(PrivateParamAccessor, butNot = CaseAccessor)
+              || aliasSym.isAllOf(Protected | ParamAccessor, butNot = CaseAccessor) && m.owner.is(Given)
+            if checking && !infos.refs(alias.symbol) then
+              warnAt(pos)(UnusedSymbol.implicitParams)
         else
-          (Nil, Nil)
-      val sortedPrivateDefs = unusedPrivates.filterNot(d => containsSyntheticSuffix(d.symbol)).map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.PrivateMembers))
-      val unsetPrivateDefs = usedPrivates.filter(isUnsetVarDef).map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.UnsetPrivates))
-      val sortedPatVars =
-        if ctx.settings.WunusedHas.patvars then
-          patVarsInScope.toList
-            .filterNot(d => d.symbol.usedDefContains)
-            .filterNot(d => containsSyntheticSuffix(d.symbol))
-            .filterNot(d => isUsedInPosition(d.symbol.name, d.span))
-            .map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.PatVars))
+          warnAt(pos)(UnusedSymbol.implicitParams)
+
+    def checkLocal(sym: Symbol, pos: SrcPos) =
+      if ctx.settings.WunusedHas.locals
+        && !sym.is(InlineProxy)
+        && !sym.isCanEqual
+      then
+        warnAt(pos)(UnusedSymbol.localDefs)
+
+    def checkPatvars() =
+      // convert the one non-synthetic span so all are comparable
+      def uniformPos(sym: Symbol, pos: SrcPos): SrcPos =
+        if pos.span.isSynthetic then pos else pos.sourcePos.withSpan(pos.span.toSynthetic)
+      // patvars in for comprehensions share the pos of where the name was introduced
+      val byPos = infos.pats.groupMap(uniformPos(_, _))((sym, pos) => sym)
+      for (pos, syms) <- byPos if !syms.exists(_.hasAnnotation(defn.UnusedAnnot)) do
+        if !syms.exists(infos.refs(_)) then
+          if !syms.exists(v => !v.isLocal && !v.is(Private)) then
+            warnAt(pos)(UnusedSymbol.patVars)
+        else if syms.exists(_.is(Mutable)) then // check unassigned var
+          val sym = // recover the original
+            if syms.size == 1 then syms.head
+            else infos.pats.find((s, p) => syms.contains(s) && !p.span.isSynthetic).map(_._1).getOrElse(syms.head)
+          if sym.is(Mutable) && !infos.asss(sym) then
+            if sym.isLocalToBlock then
+              warnAt(pos)(UnusedSymbol.unsetLocals)
+            else if sym.is(Private) then
+              warnAt(pos)(UnusedSymbol.unsetPrivates)
+
+    def checkImports() =
+      // TODO check for unused masking import
+      import scala.jdk.CollectionConverters.given
+      import Rewrites.ActionPatch
+      type ImpSel = (Import, ImportSelector)
+      def isUsable(imp: Import, sel: ImportSelector): Boolean =
+        sel.isImportExclusion || infos.sels.containsKey(sel) || imp.isLoose(sel)
+      def warnImport(warnable: ImpSel, actions: List[CodeAction] = Nil): Unit =
+        val (imp, sel) = warnable
+        val msg = UnusedSymbol.imports(actions)
+        // example collection.mutable.{Map as MutMap}
+        val origin = cpy.Import(imp)(imp.expr, List(sel)).show(using ctx.withoutColors).stripPrefix("import ")
+        warnAt(sel.srcPos)(msg, origin)
+
+      if !actionable then
+        for imp <- infos.imps.keySet.nn.asScala; sel <- imp.selectors if !isUsable(imp, sel) do
+          warnImport(imp -> sel)
+      else
+        // If the rest of the line is blank, include it in the final edit position. (Delete trailing whitespace.)
+        // If for deletion, and the prefix of the line is also blank, then include that, too. (Del blank line.)
+        def editPosAt(srcPos: SrcPos, forDeletion: Boolean): SrcPos =
+          val start = srcPos.span.start
+          val end = srcPos.span.end
+          val content = srcPos.sourcePos.source.content()
+          val prev = content.lastIndexWhere(c => !isWhitespace(c), end = start - 1)
+          val emptyLeft = prev < 0 || isLineBreakChar(content(prev))
+          val next = content.indexWhere(c => !isWhitespace(c), from = end)
+          val emptyRight = next < 0 || isLineBreakChar(content(next))
+          val deleteLine = emptyLeft && emptyRight && forDeletion
+          val bump = if (deleteLine) 1 else 0 // todo improve to include offset of next line, endline + 1
+          val p0 = srcPos.span
+          val p1 = if (next >= 0 && emptyRight) p0.withEnd(next + bump) else p0
+          val p2 = if (deleteLine) p1.withStart(prev + 1) else p1
+          srcPos.sourcePos.withSpan(p2)
+        def actionsOf(actions: (SrcPos, String)*): List[CodeAction] =
+          val patches = actions.map((srcPos, replacement) => ActionPatch(srcPos.sourcePos, replacement)).toList
+          List(CodeAction(title = "unused import", description = Some("remove import"), patches))
+        def replace(editPos: SrcPos)(replacement: String): List[CodeAction] = actionsOf(editPos -> replacement)
+        def deletion(editPos: SrcPos): List[CodeAction] = actionsOf(editPos -> "")
+        def textFor(impsel: ImpSel): String =
+          val (imp, sel) = impsel
+          val content = imp.srcPos.sourcePos.source.content()
+          def textAt(pos: SrcPos) = String(content.slice(pos.span.start, pos.span.end))
+          val qual = textAt(imp.expr.srcPos) // keep original
+          val selector = textAt(sel.srcPos)  // keep original
+          s"$qual.$selector"                 // don't succumb to vagaries of show
+        // begin actionable
+        val sortedImps = infos.imps.keySet.nn.asScala.toArray.sortBy(_.srcPos.span.point) // sorted by pos
+        var index = 0
+        while index < sortedImps.length do
+          val nextImport = sortedImps.indexSatisfying(from = index + 1)(_.isPrimaryClause) // next import statement
+          if sortedImps.indexSatisfying(from = index, until = nextImport): imp =>
+              imp.selectors.exists(!isUsable(imp, _)) // check if any selector in statement was unused
+          < nextImport then
+            // if no usable selectors in the import statement, delete it entirely.
+            // if there is exactly one usable selector, then replace with just that selector (i.e., format it).
+            // else for each clause, delete it or format one selector or delete unused selectors.
+            // To delete a comma separated item, delete start-to-start, but for last item delete a preceding comma.
+            // Reminder that first clause span includes the keyword, so delete point-to-start instead.
+            val existing = sortedImps.slice(index, nextImport)
+            val (keeping, deleting) = existing.iterator.flatMap(imp => imp.selectors.map(imp -> _)).toList
+                                      .partition(isUsable(_, _))
+            if keeping.isEmpty then
+              val editPos = existing.head.srcPos.sourcePos.withSpan:
+                Span(start = existing.head.srcPos.span.start, end = existing.last.srcPos.span.end)
+              deleting.init.foreach(warnImport(_))
+              warnImport(deleting.last, deletion(editPosAt(editPos, forDeletion = true)))
+            else if keeping.lengthIs == 1 then
+              val editPos = existing.head.srcPos.sourcePos.withSpan:
+                Span(start = existing.head.srcPos.span.start, end = existing.last.srcPos.span.end)
+              deleting.init.foreach(warnImport(_))
+              val text = s"import ${textFor(keeping.head)}"
+              warnImport(deleting.last, replace(editPosAt(editPos, forDeletion = false))(text))
+            else
+              val lostClauses = existing.iterator.filter(imp => !keeping.exists((i, _) => imp eq i)).toList
+              for imp <- lostClauses do
+                val actions =
+                  if imp == existing.last then
+                    val content = imp.srcPos.sourcePos.source.content()
+                    val prev = existing.lastIndexWhere(i0 => keeping.exists((i, _) => i == i0))
+                    val comma = content.indexOf(',', from = existing(prev).srcPos.span.end)
+                    val commaPos = imp.srcPos.sourcePos.withSpan:
+                      Span(start = comma, end = existing(prev + 1).srcPos.span.start)
+                    val srcPos = imp.srcPos
+                    val editPos = srcPos.sourcePos.withSpan: // exclude keyword
+                      srcPos.span.withStart(srcPos.span.point)
+                    actionsOf(commaPos -> "", editPos -> "")
+                  else
+                    val impIndex = existing.indexOf(imp)
+                    val editPos = imp.srcPos.sourcePos.withSpan: // exclude keyword
+                      Span(start = imp.srcPos.span.point, end = existing(impIndex + 1).srcPos.span.start)
+                    deletion(editPos)
+                imp.selectors.init.foreach(sel => warnImport(imp -> sel))
+                warnImport(imp -> imp.selectors.last, actions)
+              val singletons = existing.iterator.filter(imp => keeping.count((i, _) => imp eq i) == 1).toList
+              var seen = List.empty[Import]
+              for impsel <- deleting do
+                val (imp, sel) = impsel
+                if singletons.contains(imp) then
+                  if seen.contains(imp) then
+                    warnImport(impsel)
+                  else
+                    seen ::= imp
+                    val editPos = imp.srcPos.sourcePos.withSpan:
+                      Span(start = imp.srcPos.span.point, end = imp.srcPos.span.end) // exclude keyword
+                    val text = textFor(keeping.find((i, _) => imp eq i).get)
+                    warnImport(impsel, replace(editPosAt(editPos, forDeletion = false))(text))
+                else if !lostClauses.contains(imp) then
+                  val actions =
+                    if sel == imp.selectors.last then
+                      val content = sel.srcPos.sourcePos.source.content()
+                      val prev = imp.selectors.lastIndexWhere(s0 => keeping.exists((_, s) => s == s0))
+                      val comma = content.indexOf(',', from = imp.selectors(prev).srcPos.span.end)
+                      val commaPos = sel.srcPos.sourcePos.withSpan:
+                        Span(start = comma, end = imp.selectors(prev + 1).srcPos.span.start)
+                      val editPos = sel.srcPos
+                      actionsOf(commaPos -> "", editPos -> "")
+                    else
+                      val selIndex = imp.selectors.indexOf(sel)
+                      val editPos = sel.srcPos.sourcePos.withSpan:
+                        sel.srcPos.span.withEnd(imp.selectors(selIndex + 1).srcPos.span.start)
+                      deletion(editPos)
+                  warnImport(impsel, actions)
+          end if
+          index = nextImport
+        end while
+
+    // begin
+    for (sym, pos) <- infos.defs.iterator if !sym.hasAnnotation(defn.UnusedAnnot) do
+      if infos.refs(sym) then
+        checkUnassigned(sym, pos)
+      else if sym.is(Private, butNot = ParamAccessor) then
+        checkPrivate(sym, pos)
+      else if sym.is(Param, butNot = Given | Implicit) then
+        checkParam(sym, pos)
+      else if sym.is(Param) then // Given | Implicit
+        checkImplicit(sym, pos)
+      else if sym.isLocalToBlock then
+        checkLocal(sym, pos)
+
+    if ctx.settings.WunusedHas.patvars then
+      checkPatvars()
+
+    if ctx.settings.WunusedHas.imports || ctx.settings.WunusedHas.strictNoImplicitWarn then
+      checkImports()
+
+    warnings.result().sortBy(_._2.span.point)
+  end warnings
+
+  // Specific exclusions
+  def ignoreTree(tree: Tree): Boolean =
+    tree.hasAttachment(ForArtifact) || tree.hasAttachment(Ignore)
+
+  // The RHS of a def is too trivial to warn about unused params, e.g. def f(x: Int) = ???
+  def isUnconsuming(rhs: Tree)(using Context): Boolean =
+        rhs.symbol == defn.Predef_undefined
+     || rhs.tpe =:= defn.NothingType // compiletime.error
+     || rhs.isInstanceOf[Literal] // 42
+     || rhs.tpe.match
+        case ConstantType(_) => true
+        case tp: TermRef => tp.underlying.classSymbol.is(Module) // Scala 2 SingleType
+        case _ => false
+     //|| isPurePath(rhs) // a bit strong
+     || rhs.match
+        case Block((dd @ DefDef(anonfun, paramss, _, _)) :: Nil, Closure(Nil, Ident(nm), _)) =>
+             anonfun == nm // isAnonymousFunctionName(anonfun)
+          && paramss.match
+             case (ValDef(contextual, _, _) :: Nil) :: Nil =>
+                  contextual.is(ContextFunctionParamName)
+               && isUnconsuming(dd.rhs) // rhs was wrapped in a context function
+             case _ => false
+        case Block(Nil, Literal(u)) => u.tpe =:= defn.UnitType // def f(x: X) = {}
+        case This(_) => true
+        case Ident(_) => rhs.symbol.is(ParamAccessor)
+        case Typed(rhs, _) => isUnconsuming(rhs)
+        case _ => false
+
+  def allowVariableBindings(ok: List[Name], args: List[Tree]): Unit =
+    ok.zip(args).foreach:
+      case (param, arg @ Bind(p, _)) if param == p => arg.withAttachment(NoWarn, ())
+      case _ =>
+
+  // NoWarn Binds if the name matches a "canonical" name, e.g. case element name
+  val nowarner = new TreeTraverser:
+    def traverse(tree: Tree)(using Context) = tree match
+      case UnApply(fun, _, args) =>
+        val unapplied = tree.tpe.finalResultType.dealias.typeSymbol
+        if unapplied.is(CaseClass) then
+          allowVariableBindings(unapplied.primaryConstructor.info.firstParamNames, args)
+        else if fun.symbol == defn.PairClass_unapply then
+          val ok = fun.symbol.info match
+            case PolyType(tycon, MethodTpe(_, _, AppliedType(_, tprefs))) =>
+              tprefs.collect:
+                case ref: TypeParamRef => termName(ref.binder.paramNames(ref.paramNum).toString.toLowerCase.nn)
+            case _ => Nil
+          allowVariableBindings(ok, args)
+        else if fun.symbol == defn.TypeTest_unapply then
+          () // just recurse into args
         else
-          Nil
-      val warnings =
-        sortedImp :::
-        sortedLocalDefs :::
-        sortedExplicitParams :::
-        sortedImplicitParams :::
-        sortedPrivateDefs :::
-        sortedPatVars :::
-        unsetLocalDefs :::
-        unsetPrivateDefs
-      UnusedResult(warnings.toSet)
-    end getUnused
-    //============================ HELPERS ====================================
-
-
-    /**
-     * Checks if import selects a def that is transparent and inline
-     */
-    private def isTransparentAndInline(imp: tpd.Import)(using Context): Boolean =
-      imp.selectors.exists { sel =>
-        val qual = imp.expr
-        val importedMembers = qual.tpe.member(sel.name).alternatives.map(_.symbol)
-        importedMembers.exists(s => s.is(Transparent) && s.is(Inline))
+          if unapplied.exists && unapplied.owner == defn.Quotes_reflectModule then
+            // cheapy search for parameter names via java reflection of Trees
+            // in lieu of drilling into requiredClass("scala.quoted.runtime.impl.QuotesImpl")
+            // ...member("reflect")...member(unapplied.name.toTypeName)
+            // with aliases into requiredModule("dotty.tools.dotc.ast.tpd")
+            val implName = s"dotty.tools.dotc.ast.Trees$$${unapplied.name}"
+            try
+              import scala.language.unsafeNulls
+              val clz = Class.forName(implName) // TODO improve to use class path or reflect
+              val ok = clz.getConstructors.head.getParameters.map(p => termName(p.getName)).toList.init
+              allowVariableBindings(ok, args)
+            catch case _: ClassNotFoundException => ()
+        args.foreach(traverse)
+      case tree => traverseChildren(tree)
+
+  extension (nm: Name)
+    inline def exists(p: Name => Boolean): Boolean = nm.ne(nme.NO_NAME) && p(nm)
+    inline def isWildcard: Boolean = nm == nme.WILDCARD || nm.is(WildcardParamName)
+
+  extension (tp: Type)
+    def importPrefix(using Context): Type = tp match
+      case tp: NamedType => tp.prefix
+      case tp: ClassInfo => tp.prefix
+      case tp: TypeProxy => tp.superType.normalizedPrefix
+      case _ => NoType
+    def underlyingPrefix(using Context): Type = tp match
+      case tp: NamedType => tp.prefix
+      case tp: ClassInfo => tp.prefix
+      case tp: TypeProxy => tp.underlying.underlyingPrefix
+      case _ => NoType
+    def skipPackageObject(using Context): Type =
+      if tp.typeSymbol.isPackageObject then tp.underlyingPrefix else tp
+    def underlying(using Context): Type = tp match
+      case tp: TypeProxy => tp.underlying
+      case _ => tp
+
+  private val serializationNames: Set[TermName] =
+    Set("readResolve", "readObject", "readObjectNoData", "writeObject", "writeReplace").map(termName(_))
+
+  extension (sym: Symbol)(using Context)
+    def isSerializationSupport: Boolean =
+      sym.is(Method) && serializationNames(sym.name.toTermName) && sym.owner.isClass
+        && sym.owner.derivesFrom(defn.JavaSerializableClass)
+    def isCanEqual: Boolean =
+      sym.isOneOf(GivenOrImplicit) && sym.info.finalResultType.baseClasses.exists(_.derivesFrom(defn.CanEqualClass))
+    def isMarkerTrait: Boolean =
+      sym.isClass && sym.info.allMembers.forall: d =>
+        val m = d.symbol
+        !m.isTerm || m.isSelfSym || m.is(Method) && (m.owner == defn.AnyClass || m.owner == defn.ObjectClass)
+    def isEffectivelyOverride: Boolean =
+      sym.is(Override)
+      ||
+      sym.canMatchInheritedSymbols && { // inline allOverriddenSymbols using owner.info or thisType
+        val owner = sym.owner.asClass
+        val base = if owner.classInfo.selfInfo != NoType then owner.thisType else owner.info
+        base.baseClasses.drop(1).iterator.exists(sym.overriddenSymbol(_).exists)
       }
-
-    /**
-     * Heuristic to detect synthetic suffixes in names of symbols
-     */
-    private def containsSyntheticSuffix(symbol: Symbol)(using Context): Boolean =
-      symbol.name.mangledString.contains("$")
-
-    /**
-     * Is the constructor of synthetic package object
-     * Should be ignored as it is always imported/used in package
-     * Trigger false negative on used import
-     *
-     * Without this check example:
-     *
-     * --- WITH PACKAGE : WRONG ---
-     * {{{
-     * package a:
-     *   val x: Int = 0
-     * package b:
-     *   import a.* // no warning
-     * }}}
-     * --- WITH OBJECT : OK ---
-     * {{{
-     * object a:
-     *   val x: Int = 0
-     * object b:
-     *   import a.* // unused warning
-     * }}}
-     */
-    private def isConstructorOfSynth(sym: Symbol)(using Context): Boolean =
-      sym.exists && sym.isConstructor && sym.owner.isPackageObject && sym.owner.is(Synthetic)
-
-    /**
-     * This is used to avoid reporting the parameters of the synthetic main method
-     * generated by `@main`
-     */
-    private def isSyntheticMainParam(sym: Symbol)(using Context): Boolean =
-      sym.exists && ctx.platform.isMainMethod(sym.owner) && sym.owner.is(Synthetic)
-
-    /**
-     * This is used to ignore exclusion imports (i.e. import `qual`.{`member` => _})
+  extension (sel: ImportSelector)
+    def boundTpe: Type = sel.bound match
+      case untpd.TypedSplice(tree) => tree.tpe
+      case _ => NoType
+    /** This is used to ignore exclusion imports of the form import `qual.member as _`
+     *  because `sel.isUnimport` is too broad for old style `import concurrent._`.
      */
-    private def isImportExclusion(sel: ImportSelector): Boolean = sel.renamed match
-      case untpd.Ident(name) => name == StdNames.nme.WILDCARD
+    def isImportExclusion: Boolean = sel.renamed match
+      case untpd.Ident(nme.WILDCARD) => true
       case _ => false
 
-    /**
-     * If -Wunused:strict-no-implicit-warn import and this import selector could potentially import implicit.
-     * return true
-     */
-    private def shouldSelectorBeReported(imp: tpd.Import, sel: ImportSelector)(using Context): Boolean =
-      ctx.settings.WunusedHas.strictNoImplicitWarn && (
-        sel.isWildcard ||
-        imp.expr.tpe.member(sel.name.toTermName).alternatives.exists(_.symbol.isOneOf(GivenOrImplicit)) ||
-        imp.expr.tpe.member(sel.name.toTypeName).alternatives.exists(_.symbol.isOneOf(GivenOrImplicit))
-      )
-
-    /**
-     * Ignore CanEqual imports
-     */
-    private def isImportIgnored(imp: tpd.Import, sel: ImportSelector)(using Context): Boolean =
-      (sel.isWildcard && sel.isGiven && imp.expr.tpe.allMembers.exists(p => p.symbol.typeRef.baseClasses.exists(_.derivesFrom(defn.CanEqualClass)) && p.symbol.isOneOf(GivenOrImplicit))) ||
-      (imp.expr.tpe.member(sel.name.toTermName).alternatives
-        .exists(p => p.symbol.isOneOf(GivenOrImplicit) && p.symbol.typeRef.baseClasses.exists(_.derivesFrom(defn.CanEqualClass))))
-
-    /**
-     * Ignore definitions of CanEqual given
-     */
-    private def isDefIgnored(memDef: tpd.MemberDef)(using Context): Boolean =
-      memDef.symbol.isOneOf(GivenOrImplicit) && memDef.symbol.typeRef.baseClasses.exists(_.derivesFrom(defn.CanEqualClass))
+  extension (imp: Import)
+    /** Is it the first import clause in a statement? `a.x` in `import a.x, b.{y, z}` */
+    def isPrimaryClause(using Context): Boolean =
+      val span = imp.srcPos.span
+      span.start != span.point // primary clause starts at `import` keyword
 
-    extension (tree: ImportSelector)
-      def boundTpe: Type = tree.bound match {
-        case untpd.TypedSplice(tree1) => tree1.tpe
-        case _ => NoType
-      }
-
-    extension (sym: Symbol)
-      /** is accessible without import in current context */
-      private def isAccessibleAsIdent(using Context): Boolean =
-        ctx.outersIterator.exists{ c =>
-          c.owner == sym.owner
-          || sym.owner.isClass && c.owner.isClass
-              && c.owner.thisType.baseClasses.contains(sym.owner)
-              && c.owner.thisType.member(sym.name).alternatives.contains(sym)
-        }
-
-      /** Given an import and accessibility, return selector that matches import<->symbol */
-      private def isInImport(selData: ImportSelectorData, altName: Option[Name], isDerived: Boolean)(using Context): Boolean =
-        assert(sym.exists, s"Symbol $sym does not exist")
-
-        val selector = selData.selector
-
-        if !selector.isWildcard then
-          if altName.exists(explicitName => selector.rename != explicitName.toTermName) then
-            // if there is an explicit name, it must match
-            false
-          else
-            if isDerived then
-              // See i15503i.scala, grep for "package foo.test.i17156"
-              selData.allSymbolsDealiasedForNamed.contains(dealias(sym))
-            else
-              selData.allSymbolsForNamed.contains(sym)
-        else
-          // Wildcard
-          if selData.excludedMembers.contains(altName.getOrElse(sym.name).toTermName) then
-            // Wildcard with exclusions that match the symbol
-            false
-          else if !selData.qualTpe.member(sym.name).hasAltWith(_.symbol == sym) then
-            // The qualifier does not have the target symbol as a member
-            false
-          else
-            if selector.isGiven then
-              // Further check that the symbol is a given or implicit and conforms to the bound
-              sym.isOneOf(Given | Implicit)
-                && (selector.bound.isEmpty || sym.info.finalResultType <:< selector.boundTpe)
-            else
-              // Normal wildcard, check that the symbol is not a given (but can be implicit)
-              !sym.is(Given)
-        end if
-      end isInImport
-
-      /** Annotated with @unused */
-      private def isUnusedAnnot(using Context): Boolean =
-        sym.annotations.exists(a => a.symbol == ctx.definitions.UnusedAnnot)
-
-      private def shouldNotReportParamOwner(using Context): Boolean =
-        if sym.exists then
-          val owner = sym.owner
-          trivialDefs(owner) || // is a trivial def
-          owner.isPrimaryConstructor ||
-          owner.annotations.exists ( // @depreacated
-            _.symbol == ctx.definitions.DeprecatedAnnot
-          ) ||
-          owner.isAllOf(Synthetic | PrivateLocal) ||
-          owner.is(Accessor) ||
-          owner.isOverriden
-        else
-          false
-
-      private def usedDefContains(using Context): Boolean =
-        sym.everySymbol.exists(usedDef.apply)
-
-      private def everySymbol(using Context): List[Symbol] =
-        List(sym, sym.companionClass, sym.companionModule, sym.moduleClass).filter(_.exists)
-
-      /** A function is overriden. Either has `override flags` or parent has a matching member (type and name) */
-      private def isOverriden(using Context): Boolean =
-        sym.is(Flags.Override) || (sym.exists && sym.owner.thisType.parents.exists(p => sym.matchingMember(p).exists))
-
-    end extension
-
-    extension (defdef: tpd.DefDef)
-      // so trivial that it never consumes params
-      private def isTrivial(using Context): Boolean =
-        val rhs = defdef.rhs
-        rhs.symbol == ctx.definitions.Predef_undefined ||
-        rhs.tpe =:= ctx.definitions.NothingType ||
-        defdef.symbol.is(Deferred) ||
-        (rhs match {
-          case _: tpd.Literal => true
-          case _ => rhs.tpe match
-            case ConstantType(_) => true
-            case tp: TermRef =>
-              // Detect Scala 2 SingleType
-              tp.underlying.classSymbol.is(Flags.Module)
-            case _ =>
-              false
-        })
-      def registerTrivial(using Context): Unit =
-        if defdef.isTrivial then
-          trivialDefs += defdef.symbol
-
-    extension (memDef: tpd.MemberDef)
-      private def isValidMemberDef(using Context): Boolean =
-        memDef.symbol.exists
-          && !memDef.symbol.isUnusedAnnot
-          && !memDef.symbol.isAllOf(Flags.AccessorCreationFlags)
-          && !memDef.name.isWildcard
-          && !memDef.symbol.owner.is(ExtensionMethod)
-
-      private def isValidParam(using Context): Boolean =
-        val sym = memDef.symbol
-        (sym.is(Param) || sym.isAllOf(PrivateParamAccessor | Local, butNot = CaseAccessor)) &&
-        !isSyntheticMainParam(sym) &&
-        !sym.shouldNotReportParamOwner
-
-      private def shouldReportPrivateDef(using Context): Boolean =
-        currScopeType.top == ScopeType.Template && !memDef.symbol.isConstructor && memDef.symbol.is(Private, butNot = SelfName | Synthetic | CaseAccessor)
-
-      private def isUnsetVarDef(using Context): Boolean =
-        val sym = memDef.symbol
-        sym.is(Mutable) && !setVars(sym)
-
-    extension (imp: tpd.Import)
-      /** Enum generate an import for its cases (but outside them), which should be ignored */
-      def isGeneratedByEnum(using Context): Boolean =
-        imp.symbol.exists && imp.symbol.owner.is(Flags.Enum, butNot = Flags.Case)
-
-    extension (thisName: Name)
-      private def isWildcard: Boolean =
-        thisName == StdNames.nme.WILDCARD || thisName.is(WildcardParamName)
-
-  end UnusedData
-
-  private object UnusedData:
-    enum ScopeType:
-      case Local
-      case Template
-      case ReplWrapper
-      case Other
-
-    object ScopeType:
-      /** return the scope corresponding to the enclosing scope of the given tree */
-      def fromTree(tree: tpd.Tree)(using Context): ScopeType = tree match
-        case tree: tpd.Template => if tree.symbol.name.isReplWrapperName then ReplWrapper else Template
-        case _:tpd.Block => Local
-        case _ => Other
-
-    final class ImportSelectorData(val qualTpe: Type, val selector: ImportSelector):
-      private var myUsed: Boolean = false
-      var excludedMembers: Set[TermName] = Set.empty
-
-      def markUsed(): Unit = myUsed = true
-
-      def isUsed: Boolean = myUsed
-
-      def markExcluded(excluded: Set[TermName]): Unit = excludedMembers ++= excluded
-
-      private var myAllSymbols: Set[Symbol] | Null = null
-
-      def allSymbolsForNamed(using Context): Set[Symbol] =
-        if myAllSymbols == null then
-          val allDenots = qualTpe.member(selector.name).alternatives ::: qualTpe.member(selector.name.toTypeName).alternatives
-          myAllSymbols = allDenots.map(_.symbol).toSet
-        myAllSymbols.uncheckedNN
-
-      private var myAllSymbolsDealiased: Set[Symbol] | Null = null
-
-      def allSymbolsDealiasedForNamed(using Context): Set[Symbol] =
-        if myAllSymbolsDealiased == null then
-          myAllSymbolsDealiased = allSymbolsForNamed.map(sym => dealias(sym))
-        myAllSymbolsDealiased.uncheckedNN
-    end ImportSelectorData
-
-    case class UnusedSymbol(pos: SrcPos, name: Name, warnType: WarnTypes)
-    /** A container for the results of the used elements analysis */
-    case class UnusedResult(warnings: Set[UnusedSymbol])
-    object UnusedResult:
-      val Empty = UnusedResult(Set.empty)
-  end UnusedData
-
-  private def dealias(symbol: Symbol)(using Context): Symbol =
-    if symbol.isType && symbol.asType.denot.isAliasType then
-      symbol.asType.typeRef.dealias.typeSymbol
-    else
-      symbol
+    /** Generated import of cases from enum companion. */
+    def isGeneratedByEnum(using Context): Boolean =
+      imp.symbol.exists && imp.symbol.owner.is(Enum, butNot = Case)
 
+    /** Under -Wunused:strict-no-implicit-warn, avoid false positives
+     *  if this selector is a wildcard that might import implicits or
+     *  specifically does import an implicit.
+     *  Similarly, import of CanEqual must not warn, as it is always witness.
+     */
+    def isLoose(sel: ImportSelector)(using Context): Boolean =
+      if ctx.settings.WunusedHas.strictNoImplicitWarn then
+        if sel.isWildcard
+          || imp.expr.tpe.member(sel.name.toTermName).hasAltWith(_.symbol.isOneOf(GivenOrImplicit))
+          || imp.expr.tpe.member(sel.name.toTypeName).hasAltWith(_.symbol.isOneOf(GivenOrImplicit))
+        then return true
+      if sel.isWildcard && sel.isGiven
+      then imp.expr.tpe.allMembers.exists(_.symbol.isCanEqual)
+      else imp.expr.tpe.member(sel.name.toTermName).hasAltWith(_.symbol.isCanEqual)
+
+  extension (pos: SrcPos)
+    def isZeroExtentSynthetic: Boolean = pos.span.isSynthetic && pos.span.isZeroExtent
+
+  extension [A <: AnyRef](arr: Array[A])
+    // returns `until` if not satisfied
+    def indexSatisfying(from: Int, until: Int = arr.length)(p: A => Boolean): Int =
+      var i = from
+      while i < until && !p(arr(i)) do
+        i += 1
+      i
 end CheckUnused
diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala
index 72a3ff7324e8..9134e9736079 100644
--- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala
+++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala
@@ -193,7 +193,9 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
                     ++ sym.annotations)
           else
             if sym.is(Param) then
-              sym.keepAnnotationsCarrying(thisPhase, Set(defn.ParamMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots)
+              // @unused is getter/setter but we want it on ordinary method params
+              if !sym.owner.is(Method) || sym.owner.isConstructor then
+                sym.keepAnnotationsCarrying(thisPhase, Set(defn.ParamMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots)
             else if sym.is(ParamAccessor) then
               sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot))
             else
diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala b/compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala
index 9e40792895c0..4922024b6c35 100644
--- a/compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala
+++ b/compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala
@@ -3,10 +3,8 @@ package transform.localopt
 
 import scala.annotation.tailrec
 import scala.collection.mutable.ListBuffer
-import scala.util.chaining.*
 import scala.util.matching.Regex.Match
 
-
 import PartialFunction.cond
 
 import dotty.tools.dotc.ast.tpd.{Match => _, *}
@@ -15,6 +13,7 @@ import dotty.tools.dotc.core.Symbols.*
 import dotty.tools.dotc.core.Types.*
 import dotty.tools.dotc.core.Phases.typerPhase
 import dotty.tools.dotc.util.Spans.Span
+import dotty.tools.dotc.util.chaining.*
 
 /** Formatter string checker. */
 class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List[Tree])(using Context):
diff --git a/compiler/src/dotty/tools/dotc/typer/Deriving.scala b/compiler/src/dotty/tools/dotc/typer/Deriving.scala
index cc940ee38e41..febb12cf1007 100644
--- a/compiler/src/dotty/tools/dotc/typer/Deriving.scala
+++ b/compiler/src/dotty/tools/dotc/typer/Deriving.scala
@@ -10,7 +10,7 @@ import Contexts.*, Symbols.*, Types.*, SymDenotations.*, Names.*, NameOps.*, Fla
 import ProtoTypes.*, ContextOps.*
 import util.Spans.*
 import util.SrcPos
-import collection.mutable
+import collection.mutable.ListBuffer
 import ErrorReporting.errorTree
 
 /** A typer mixin that implements type class derivation functionality */
@@ -25,8 +25,8 @@ trait Deriving {
    */
   class Deriver(cls: ClassSymbol, codePos: SrcPos)(using Context) {
 
-    /** A buffer for synthesized symbols for type class instances */
-    private var synthetics = new mutable.ListBuffer[Symbol]
+    /** A buffer for synthesized symbols for type class instances, with what user asked to synthesize. */
+    private val synthetics = ListBuffer.empty[(tpd.Tree, Symbol)]
 
     /** A version of Type#underlyingClassRef that works also for higher-kinded types */
     private def underlyingClassRef(tp: Type): Type = tp match {
@@ -41,7 +41,7 @@ trait Deriving {
      *  an instance with the same name does not exist already.
      *  @param  reportErrors  Report an error if an instance with the same name exists already
      */
-    private def addDerivedInstance(clsName: Name, info: Type, pos: SrcPos): Unit = {
+    private def addDerivedInstance(derived: tpd.Tree, clsName: Name, info: Type, pos: SrcPos): Unit = {
       val instanceName = "derived$".concat(clsName)
       if (ctx.denotNamed(instanceName).exists)
         report.error(em"duplicate type class derivation for $clsName", pos)
@@ -50,9 +50,8 @@ trait Deriving {
         // derived instance will have too low a priority to be selected over a freshly
         // derived instance at the summoning site.
         val flags = if info.isInstanceOf[MethodOrPoly] then GivenMethod else Given | Lazy
-        synthetics +=
-          newSymbol(ctx.owner, instanceName, flags, info, coord = pos.span)
-            .entered
+        val sym = newSymbol(ctx.owner, instanceName, flags, info, coord = pos.span).entered
+        synthetics += derived -> sym
     }
 
     /** Check derived type tree `derived` for the following well-formedness conditions:
@@ -77,7 +76,8 @@ trait Deriving {
      *  that have the same name but different prefixes through selective aliasing.
      */
     private def processDerivedInstance(derived: untpd.Tree): Unit = {
-      val originalTypeClassType = typedAheadType(derived, AnyTypeConstructorProto).tpe
+      val originalTypeClassTree = typedAheadType(derived, AnyTypeConstructorProto)
+      val originalTypeClassType = originalTypeClassTree.tpe
       val underlyingClassType = underlyingClassRef(originalTypeClassType)
       val typeClassType = checkClassType(
           underlyingClassType.orElse(originalTypeClassType),
@@ -100,7 +100,7 @@ trait Deriving {
         val derivedInfo =
           if derivedParams.isEmpty then monoInfo
           else PolyType.fromParams(derivedParams, monoInfo)
-        addDerivedInstance(originalTypeClassType.typeSymbol.name, derivedInfo, derived.srcPos)
+        addDerivedInstance(originalTypeClassTree, originalTypeClassType.typeSymbol.name, derivedInfo, derived.srcPos)
       }
 
       def deriveSingleParameter: Unit = {
@@ -312,7 +312,7 @@ trait Deriving {
         else tpd.ValDef(sym.asTerm, typeclassInstance(sym)(Nil))
       }
 
-      synthetics.map(syntheticDef).toList
+      synthetics.map((t, s) => syntheticDef(s).withAttachment(Deriving.OriginalTypeClass, t)).toList
     }
 
     def finalize(stat: tpd.TypeDef): tpd.Tree = {
@@ -321,3 +321,8 @@ trait Deriving {
     }
   }
 }
+object Deriving:
+  import dotty.tools.dotc.util.Property
+
+  /** Attachment holding the name of a type class as written by the user. */
+  val OriginalTypeClass = Property.StickyKey[tpd.Tree]
diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala
index d46c85b38e9d..38bc6960e370 100644
--- a/compiler/src/dotty/tools/dotc/typer/Typer.scala
+++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala
@@ -77,6 +77,9 @@ object Typer {
   /** Indicates that an expression is explicitly ascribed to [[Unit]] type. */
   val AscribedToUnit = new Property.StickyKey[Unit]
 
+  /** Tree adaptation lost fidelity; this attachment preserves the original tree. */
+  val AdaptedTree = new Property.StickyKey[tpd.Tree]
+
   /** An attachment on a Select node with an `apply` field indicating that the `apply`
    *  was inserted by the Typer.
    */
@@ -3057,7 +3060,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
   def typedTuple(tree: untpd.Tuple, pt: Type)(using Context): Tree = {
     val arity = tree.trees.length
     if (arity <= Definitions.MaxTupleArity)
-      typed(desugar.smallTuple(tree).withSpan(tree.span), pt)
+      typed(desugar.smallTuple(tree).withSpan(tree.span).withAttachmentsFrom(tree), pt)
     else {
       val pts =
         pt.tupleElementTypes match
@@ -4204,12 +4207,12 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
 
     /** Adapt an expression of constant type to a different constant type `tpe`. */
     def adaptConstant(tree: Tree, tpe: ConstantType): Tree = {
-      def lit = Literal(tpe.value).withSpan(tree.span)
+      def lit = Literal(tpe.value).withSpan(tree.span).withAttachment(AdaptedTree, tree)
       tree match {
         case Literal(c) => lit
         case tree @ Block(stats, expr) => tpd.cpy.Block(tree)(stats, adaptConstant(expr, tpe))
         case tree =>
-          if (isIdempotentExpr(tree)) lit // See discussion in phase Literalize why we demand isIdempotentExpr
+          if isIdempotentExpr(tree) then lit // See discussion in phase FirstTransform why we demand isIdempotentExpr
           else Block(tree :: Nil, lit)
       }
     }
diff --git a/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala b/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala
index ec88b5880745..cca7af0d2fa3 100644
--- a/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala
+++ b/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala
@@ -1,7 +1,7 @@
 package dotty.tools.dotc.util
 
 import scala.collection.mutable.ArrayBuffer
-import scala.util.chaining.*
+import dotty.tools.dotc.util.chaining.*
 
 /** A wrapper for a list of cached instances of a type `T`.
   * The wrapper is recursion-reentrant: several instances are kept, so
diff --git a/compiler/src/dotty/tools/dotc/util/SourceFile.scala b/compiler/src/dotty/tools/dotc/util/SourceFile.scala
index 246255ae3802..939690bb2ee0 100644
--- a/compiler/src/dotty/tools/dotc/util/SourceFile.scala
+++ b/compiler/src/dotty/tools/dotc/util/SourceFile.scala
@@ -13,7 +13,7 @@ import Chars.*
 import scala.annotation.internal.sharable
 import scala.collection.mutable
 import scala.collection.mutable.ArrayBuffer
-import scala.util.chaining.given
+import dotty.tools.dotc.util.chaining.*
 
 import java.io.File.separator
 import java.net.URI
diff --git a/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala b/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala
index f991005f0c43..bd5c031a65e0 100644
--- a/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala
+++ b/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala
@@ -15,7 +15,7 @@ package dotty.tools.dotc.util
 import scala.language.unsafeNulls
 
 import collection.mutable, mutable.ListBuffer
-import scala.util.chaining.given
+import dotty.tools.dotc.util.chaining.*
 import java.lang.System.lineSeparator
 
 object StackTraceOps:
diff --git a/compiler/src/dotty/tools/dotc/util/chaining.scala b/compiler/src/dotty/tools/dotc/util/chaining.scala
new file mode 100644
index 000000000000..0c61ab6e73e9
--- /dev/null
+++ b/compiler/src/dotty/tools/dotc/util/chaining.scala
@@ -0,0 +1,8 @@
+
+package dotty.tools.dotc.util
+
+object chaining:
+
+  extension [A](x: A)
+    inline def tap(inline f: A => Unit): x.type = { f(x): Unit; x }
+    inline def pipe[B](inline f: A => B): B = f(x)
diff --git a/compiler/src/dotty/tools/repl/ReplCompiler.scala b/compiler/src/dotty/tools/repl/ReplCompiler.scala
index 5c8dcc2616b7..e34bb229a727 100644
--- a/compiler/src/dotty/tools/repl/ReplCompiler.scala
+++ b/compiler/src/dotty/tools/repl/ReplCompiler.scala
@@ -11,16 +11,16 @@ import dotty.tools.dotc.core.Phases.Phase
 import dotty.tools.dotc.core.StdNames.*
 import dotty.tools.dotc.core.Symbols.*
 import dotty.tools.dotc.reporting.Diagnostic
-import dotty.tools.dotc.transform.PostTyper
+import dotty.tools.dotc.transform.{CheckUnused, CheckShadowing, PostTyper}
 import dotty.tools.dotc.typer.ImportInfo.{withRootImports, RootRef}
 import dotty.tools.dotc.typer.TyperPhase
 import dotty.tools.dotc.util.Spans.*
 import dotty.tools.dotc.util.{ParsedComment, Property, SourceFile}
 import dotty.tools.dotc.{CompilationUnit, Compiler, Run}
 import dotty.tools.repl.results.*
+import dotty.tools.dotc.util.chaining.*
 
 import scala.collection.mutable
-import scala.util.chaining.given
 
 /** This subclass of `Compiler` is adapted for use in the REPL.
  *
@@ -36,6 +36,7 @@ class ReplCompiler extends Compiler:
     List(Parser()),
     List(ReplPhase()),
     List(TyperPhase(addRootImports = false)),
+    List(CheckUnused.PostTyper(), CheckShadowing()),
     List(CollectTopLevelImports()),
     List(PostTyper()),
   )
diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala
index 71bf5a7e7844..1b33c52416ea 100644
--- a/compiler/test/dotty/tools/dotc/CompilationTests.scala
+++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala
@@ -69,6 +69,7 @@ class CompilationTests {
       compileFile("tests/rewrites/i17399.scala", unindentOptions.and("-rewrite")),
       compileFile("tests/rewrites/i20002.scala", defaultOptions.and("-indent", "-rewrite")),
       compileFile("tests/rewrites/i21382.scala", defaultOptions.and("-indent", "-rewrite")),
+      compileFile("tests/rewrites/unused.scala", defaultOptions.and("-rewrite", "-Wunused:all")),
     ).checkRewrites()
   }
 
diff --git a/compiler/test/dotty/tools/utils.scala b/compiler/test/dotty/tools/utils.scala
index a8c480088e08..49ea2d4d3873 100644
--- a/compiler/test/dotty/tools/utils.scala
+++ b/compiler/test/dotty/tools/utils.scala
@@ -101,10 +101,10 @@ def toolArgsParse(lines: List[String], filename: Option[String]): List[(String,S
     case toolArg(name, args) => List((name, args))
     case _ => Nil
   } ++
-  lines.flatMap { 
+  lines.flatMap {
     case directiveOptionsArg(args) => List(("scalac", args))
     case directiveJavacOptions(args) => List(("javac", args))
-    case _ => Nil 
+    case _ => Nil
   }
 
 import org.junit.Test
diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala
index 7d32b3d6c9fa..e15103046fbf 100644
--- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala
+++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala
@@ -746,13 +746,14 @@ trait ParallelTesting extends RunnerOrchestration { self =>
       diffCheckfile(testSource, reporters, logger)
 
     override def maybeFailureMessage(testSource: TestSource, reporters: Seq[TestReporter]): Option[String] =
-      lazy val (map, expCount) = getWarnMapAndExpectedCount(testSource.sourceFiles.toIndexedSeq)
+      lazy val (expected, expCount) = getWarnMapAndExpectedCount(testSource.sourceFiles.toIndexedSeq)
       lazy val obtCount = reporters.foldLeft(0)(_ + _.warningCount)
-      lazy val (expected, unexpected) = getMissingExpectedWarnings(map, reporters.iterator.flatMap(_.diagnostics))
-      lazy val diagnostics = reporters.flatMap(_.diagnostics.toSeq.sortBy(_.pos.line).map(e => s" at ${e.pos.line + 1}: ${e.message}"))
-      def showLines(title: String, lines: Seq[String]) = if lines.isEmpty then "" else title + lines.mkString("\n", "\n", "")
-      def hasMissingAnnotations = expected.nonEmpty || unexpected.nonEmpty
-      def showDiagnostics = showLines("-> following the diagnostics:", diagnostics)
+      lazy val (unfulfilled, unexpected) = getMissingExpectedWarnings(expected, diagnostics.iterator)
+      lazy val diagnostics = reporters.flatMap(_.diagnostics.toSeq.sortBy(_.pos.line))
+      lazy val messages = diagnostics.map(d => s" at ${d.pos.line + 1}: ${d.message}")
+      def showLines(title: String, lines: Seq[String]) = if lines.isEmpty then "" else lines.mkString(s"$title\n", "\n", "")
+      def hasMissingAnnotations = unfulfilled.nonEmpty || unexpected.nonEmpty
+      def showDiagnostics = showLines("-> following the diagnostics:", messages)
       Option:
         if reporters.exists(_.errorCount > 0) then
           s"""Compilation failed for: ${testSource.title}
@@ -761,58 +762,63 @@ trait ParallelTesting extends RunnerOrchestration { self =>
         else if expCount != obtCount then
           s"""|Wrong number of warnings encountered when compiling $testSource
               |expected: $expCount, actual: $obtCount
-              |${showLines("Unfulfilled expectations:", expected)}
+              |${showLines("Unfulfilled expectations:", unfulfilled)}
               |${showLines("Unexpected warnings:", unexpected)}
               |$showDiagnostics
               |""".stripMargin.trim.linesIterator.mkString("\n", "\n", "")
-        else if hasMissingAnnotations then s"\nWarnings found on incorrect row numbers when compiling $testSource\n$showDiagnostics"
-        else if !map.isEmpty then s"\nExpected warnings(s) have {<warning position>=<unreported warning>}: $map"
+        else if hasMissingAnnotations then
+          s"""|Warnings found on incorrect row numbers when compiling $testSource
+              |${showLines("Unfulfilled expectations:", unfulfilled)}
+              |${showLines("Unexpected warnings:", unexpected)}
+              |$showDiagnostics
+              |""".stripMargin.trim.linesIterator.mkString("\n", "\n", "")
+        else if !expected.isEmpty then s"\nExpected warnings(s) have {<warning position>=<unreported warning>}: $expected"
         else null
     end maybeFailureMessage
 
     def getWarnMapAndExpectedCount(files: Seq[JFile]): (HashMap[String, Integer], Int) =
-      val comment = raw"//( *)(nopos-)?warn".r
-      val map = new HashMap[String, Integer]()
+      val comment = raw"//(?: *)(nopos-)?warn".r
+      val map = HashMap[String, Integer]()
       var count = 0
       def bump(key: String): Unit =
         map.get(key) match
           case null => map.put(key, 1)
           case n    => map.put(key, n+1)
         count += 1
-      files.filter(isSourceFile).foreach { file =>
-        Using(Source.fromFile(file, StandardCharsets.UTF_8.name)) { source =>
-          source.getLines.zipWithIndex.foreach { case (line, lineNbr) =>
-            comment.findAllMatchIn(line).foreach { m =>
-              m.group(2) match
-                case "nopos-" =>
-                  bump("nopos")
-                case _ => bump(s"${file.getPath}:${lineNbr+1}")
-            }
-          }
-        }.get
-      }
+      for file <- files if isSourceFile(file) do
+        Using.resource(Source.fromFile(file, StandardCharsets.UTF_8.name)) { source =>
+          source.getLines.zipWithIndex.foreach: (line, lineNbr) =>
+            comment.findAllMatchIn(line).foreach:
+              case comment("nopos-") => bump("nopos")
+              case _                 => bump(s"${file.getPath}:${lineNbr+1}")
+        }
+      end for
       (map, count)
 
-    def getMissingExpectedWarnings(map: HashMap[String, Integer], reporterWarnings: Iterator[Diagnostic]): (List[String], List[String]) =
-      val unexpected, unpositioned = ListBuffer.empty[String]
+    // return unfulfilled expected warnings and unexpected diagnostics
+    def getMissingExpectedWarnings(expected: HashMap[String, Integer], reporterWarnings: Iterator[Diagnostic]): (List[String], List[String]) =
+      val unexpected = ListBuffer.empty[String]
       def relativize(path: String): String = path.split(JFile.separatorChar).dropWhile(_ != "tests").mkString(JFile.separator)
       def seenAt(key: String): Boolean =
-        map.get(key) match
+        expected.get(key) match
           case null => false
-          case 1 => map.remove(key) ; true
-          case n => map.put(key, n - 1) ; true
+          case 1 => expected.remove(key); true
+          case n => expected.put(key, n - 1); true
       def sawDiagnostic(d: Diagnostic): Unit =
         val srcpos = d.pos.nonInlined
         if srcpos.exists then
           val key = s"${relativize(srcpos.source.file.toString())}:${srcpos.line + 1}"
           if !seenAt(key) then unexpected += key
         else
-          if(!seenAt("nopos")) unpositioned += relativize(srcpos.source.file.toString())
+          if !seenAt("nopos") then unexpected += relativize(srcpos.source.file.toString)
 
       reporterWarnings.foreach(sawDiagnostic)
 
-      (map.asScala.keys.toList, (unexpected ++ unpositioned).toList)
+      val splitter = raw"(?:[^:]*):(\d+)".r
+      val unfulfilled = expected.asScala.keys.toList.sortBy { case splitter(n) => n.toInt case _ => -1 }
+      (unfulfilled, unexpected.toList)
     end getMissingExpectedWarnings
+  end WarnTest
 
   private final class RewriteTest(testSources: List[TestSource], checkFiles: Map[JFile, JFile], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting)
   extends Test(testSources, times, threadLimit, suppressAllOutput) {
@@ -947,8 +953,8 @@ trait ParallelTesting extends RunnerOrchestration { self =>
       def seenAt(key: String): Boolean =
         errorMap.get(key) match
           case null => false
-          case 1 => errorMap.remove(key) ; true
-          case n => errorMap.put(key, n - 1) ; true
+          case 1 => errorMap.remove(key); true
+          case n => errorMap.put(key, n - 1); true
       def sawDiagnostic(d: Diagnostic): Unit =
         d.pos.nonInlined match
           case srcpos if srcpos.exists =>
diff --git a/library/src/scala/deriving/Mirror.scala b/library/src/scala/deriving/Mirror.scala
index 57453a516567..a7477cf0fb2d 100644
--- a/library/src/scala/deriving/Mirror.scala
+++ b/library/src/scala/deriving/Mirror.scala
@@ -52,7 +52,7 @@ object Mirror {
 
   extension [T](p: ProductOf[T])
     /** Create a new instance of type `T` with elements taken from product `a`. */
-    def fromProductTyped[A <: scala.Product, Elems <: p.MirroredElemTypes](a: A)(using m: ProductOf[A] { type MirroredElemTypes = Elems }): T =
+    def fromProductTyped[A <: scala.Product, Elems <: p.MirroredElemTypes](a: A)(using ProductOf[A] { type MirroredElemTypes = Elems }): T =
       p.fromProduct(a)
 
     /** Create a new instance of type `T` with elements taken from tuple `t`. */
diff --git a/library/src/scala/quoted/Quotes.scala b/library/src/scala/quoted/Quotes.scala
index 873d5e1e993e..c5d0022b6cf5 100644
--- a/library/src/scala/quoted/Quotes.scala
+++ b/library/src/scala/quoted/Quotes.scala
@@ -1,7 +1,6 @@
 package scala.quoted
 
-import scala.annotation.experimental
-import scala.annotation.implicitNotFound
+import scala.annotation.{experimental, implicitNotFound, unused}
 import scala.reflect.TypeTest
 
 /** Current Quotes in scope
@@ -4797,7 +4796,7 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
             foldTree(foldTree(foldTree(x, cond)(owner), thenp)(owner), elsep)(owner)
           case While(cond, body) =>
             foldTree(foldTree(x, cond)(owner), body)(owner)
-          case Closure(meth, tpt) =>
+          case Closure(meth, _) =>
             foldTree(x, meth)(owner)
           case Match(selector, cases) =>
             foldTrees(foldTree(x, selector)(owner), cases)(owner)
@@ -4875,7 +4874,7 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>
 
       def traverseTree(tree: Tree)(owner: Symbol): Unit = traverseTreeChildren(tree)(owner)
 
-      def foldTree(x: Unit, tree: Tree)(owner: Symbol): Unit = traverseTree(tree)(owner)
+      def foldTree(@unused x: Unit, tree: Tree)(owner: Symbol): Unit = traverseTree(tree)(owner)
 
       protected def traverseTreeChildren(tree: Tree)(owner: Symbol): Unit = foldOverTree((), tree)(owner)
 
diff --git a/library/src/scala/runtime/Arrays.scala b/library/src/scala/runtime/Arrays.scala
index 2d98caea4df8..97ff589c6610 100644
--- a/library/src/scala/runtime/Arrays.scala
+++ b/library/src/scala/runtime/Arrays.scala
@@ -1,5 +1,6 @@
 package scala.runtime
 
+import scala.annotation.unused
 import scala.reflect.ClassTag
 
 import java.lang.{reflect => jlr}
@@ -26,6 +27,6 @@ object Arrays {
 
   /** Create an array of a reference type T.
    */
-  def newArray[Arr](componentType: Class[_], returnType: Class[Arr], dimensions: Array[Int]): Arr =
+  def newArray[Arr](componentType: Class[_], @unused returnType: Class[Arr], dimensions: Array[Int]): Arr =
     jlr.Array.newInstance(componentType, dimensions: _*).asInstanceOf[Arr]
 }
diff --git a/library/src/scala/runtime/LazyVals.scala b/library/src/scala/runtime/LazyVals.scala
index 33ccea4325a5..f3225a4acbd6 100644
--- a/library/src/scala/runtime/LazyVals.scala
+++ b/library/src/scala/runtime/LazyVals.scala
@@ -96,13 +96,13 @@ object LazyVals {
       println(s"CAS($t, $offset, $e, $v, $ord)")
     val mask = ~(LAZY_VAL_MASK << ord * BITS_PER_LAZY_VAL)
     val n = (e & mask) | (v.toLong << (ord * BITS_PER_LAZY_VAL))
-    unsafe.compareAndSwapLong(t, offset, e, n)
+    unsafe.compareAndSwapLong(t, offset, e, n): @nowarn("cat=deprecation")
   }
 
   def objCAS(t: Object, offset: Long, exp: Object, n: Object): Boolean = {
     if (debug)
       println(s"objCAS($t, $exp, $n)")
-    unsafe.compareAndSwapObject(t, offset, exp, n)
+    unsafe.compareAndSwapObject(t, offset, exp, n): @nowarn("cat=deprecation")
   }
 
   def setFlag(t: Object, offset: Long, v: Int, ord: Int): Unit = {
@@ -147,7 +147,7 @@ object LazyVals {
   def get(t: Object, off: Long): Long = {
     if (debug)
       println(s"get($t, $off)")
-    unsafe.getLongVolatile(t, off)
+    unsafe.getLongVolatile(t, off): @nowarn("cat=deprecation")
   }
 
   // kept for backward compatibility
diff --git a/project/Build.scala b/project/Build.scala
index bce4ad9819eb..82d9cb786811 100644
--- a/project/Build.scala
+++ b/project/Build.scala
@@ -240,7 +240,9 @@ object Build {
       "-deprecation",
       "-unchecked",
       //"-Wconf:cat=deprecation&msg=Unsafe:s",    // example usage
-      "-Xfatal-warnings",                         // -Werror in modern usage
+      "-Werror",
+      //"-Wunused:all",
+      //"-rewrite", // requires -Werror:false since no rewrites are applied with errors
       "-encoding", "UTF8",
       "-language:implicitConversions",
     ),
@@ -1068,7 +1070,7 @@ object Build {
         )
       },
       Compile / doc / scalacOptions += "-Ydocument-synthetic-types",
-      scalacOptions -= "-Xfatal-warnings",
+      scalacOptions += "-Werror:false",
       ivyConfigurations += SourceDeps.hide,
       transitiveClassifiers := Seq("sources"),
       libraryDependencies +=
@@ -1351,7 +1353,7 @@ object Build {
     dependsOn(`scala3-library-bootstrappedJS`).
     settings(
       bspEnabled := false,
-      scalacOptions --= Seq("-Xfatal-warnings", "-deprecation"),
+      scalacOptions --= Seq("-Werror", "-deprecation"),
 
       // Required to run Scala.js tests.
       Test / fork := false,
diff --git a/tasty/src/dotty/tools/tasty/TastyReader.scala b/tasty/src/dotty/tools/tasty/TastyReader.scala
index 31407f7a4ab8..18e94867e26e 100644
--- a/tasty/src/dotty/tools/tasty/TastyReader.scala
+++ b/tasty/src/dotty/tools/tasty/TastyReader.scala
@@ -99,7 +99,7 @@ class TastyReader(val bytes: Array[Byte], start: Int, end: Int, val base: Int =
   /** Read an uncompressed Long stored in 8 bytes in big endian format */
   def readUncompressedLong(): Long = {
     var x: Long = 0
-    for (i <- 0 to 7)
+    for (_ <- 0 to 7)
       x = (x << 8) | (readByte() & 0xff)
     x
   }
diff --git a/tests/pos/i11729.scala b/tests/pos/i11729.scala
index e97b285ac6a2..79d3174dc2e9 100644
--- a/tests/pos/i11729.scala
+++ b/tests/pos/i11729.scala
@@ -6,7 +6,7 @@ type Return[X] = X match
 
 object Return:
   def apply[A](a:A):Return[A] = a match
-    case a: List[t] => a
+    case a: List[?] => a
     case a: Any => List(a)
 
 object Test1:
@@ -18,7 +18,7 @@ type Boxed[X] = X match
    case Any => Box[X]
 
 def box[X](x: X): Boxed[X] = x match
-   case b: Box[t] => b
+   case b: Box[?] => b
    case x: Any => Box(x)
 
 case class Box[A](a:A):
diff --git a/tests/pos/i17631.scala b/tests/pos/i17631.scala
index 7b8a064493df..ddcb71354968 100644
--- a/tests/pos/i17631.scala
+++ b/tests/pos/i17631.scala
@@ -1,4 +1,4 @@
-//> using options -Xfatal-warnings -Wunused:all -deprecation -feature
+//> using options -Werror -Wunused:all -deprecation -feature
 
 object foo {
   type Bar
@@ -32,3 +32,23 @@ object Main {
     (bad1, bad2)
   }
 }
+
+def `i18388`: Unit =
+  def func(pred: [A] => A => Boolean): Unit =
+    val _ = pred
+    ()
+  val _ = func
+
+trait L[T]:
+  type E
+
+def `i19748` =
+  type Warn1 = [T] => (l: L[T]) => T => l.E
+  type Warn2 = [T] => L[T] => T
+  type Warn3 = [T] => T => T
+  def use(x: (Warn1, Warn2, Warn3)) = x
+  use
+
+type NoWarning1 = [T] => (l: L[T]) => T => l.E
+type NoWarning2 = [T] => L[T] => T
+type NoWarning3 = [T] => T => T
diff --git a/tests/pos/i18366.scala b/tests/pos/i18366.scala
deleted file mode 100644
index 698510ad13a2..000000000000
--- a/tests/pos/i18366.scala
+++ /dev/null
@@ -1,10 +0,0 @@
-//> using options -Xfatal-warnings -Wunused:all
-
-trait Builder {
-  def foo(): Unit
-}
-
-def repro =
-  val builder: Builder = ???
-  import builder.{foo => bar}
-  bar()
\ No newline at end of file
diff --git a/tests/pos/i3323.scala b/tests/pos/i3323.scala
deleted file mode 100644
index 94d072d4a2fc..000000000000
--- a/tests/pos/i3323.scala
+++ /dev/null
@@ -1,9 +0,0 @@
-//> using options -Xfatal-warnings -deprecation -feature
-
-class Foo {
-  def foo[A](lss: List[List[A]]): Unit = {
-    lss match {
-      case xss: List[List[A]] =>
-    }
-  }
-}
diff --git a/tests/pos/tuple-exaustivity.scala b/tests/pos/tuple-exaustivity.scala
deleted file mode 100644
index a27267fc89e5..000000000000
--- a/tests/pos/tuple-exaustivity.scala
+++ /dev/null
@@ -1,6 +0,0 @@
-//> using options -Xfatal-warnings -deprecation -feature
-
-def test(t: Tuple)  =
-  t match
-    case Tuple() =>
-    case head *: tail =>
diff --git a/tests/rewrites/ambiguous-named-tuple-assignment.check b/tests/rewrites/ambiguous-named-tuple-assignment.check
new file mode 100644
index 000000000000..00e6cc4112f1
--- /dev/null
+++ b/tests/rewrites/ambiguous-named-tuple-assignment.check
@@ -0,0 +1,19 @@
+import scala.language.experimental.namedTuples
+
+object i21770:
+  def f(g: Int  => Unit) = g(0)
+  var cache: Option[Int] = None
+  f(i => {cache = Some(i)})
+  
+object i21861:
+  var age: Int = 28
+  {
+    age = 29
+  }
+  
+  
+object i21861c:
+  def age: Int = ???
+  def age_=(x: Int): Unit = ()
+  age = 29
+  { age = 29 }
diff --git a/tests/rewrites/ambiguous-named-tuple-assignment.scala b/tests/rewrites/ambiguous-named-tuple-assignment.scala
new file mode 100644
index 000000000000..e9685b7b58cf
--- /dev/null
+++ b/tests/rewrites/ambiguous-named-tuple-assignment.scala
@@ -0,0 +1,19 @@
+import scala.language.experimental.namedTuples
+
+object i21770:
+  def f(g: Int  => Unit) = g(0)
+  var cache: Option[Int] = None
+  f(i => (cache = Some(i)))
+  
+object i21861:
+  var age: Int = 28
+  (
+    age = 29
+  )
+  
+  
+object i21861c:
+  def age: Int = ???
+  def age_=(x: Int): Unit = ()
+  age = 29
+  ( age = 29 )
diff --git a/tests/rewrites/unused.check b/tests/rewrites/unused.check
new file mode 100644
index 000000000000..1ff93bfb6ef2
--- /dev/null
+++ b/tests/rewrites/unused.check
@@ -0,0 +1,55 @@
+
+//> using options -Wunused:all
+
+package p1:
+  import java.lang.Runnable
+  class C extends Runnable { def run() = () }
+
+package p2:
+  import java.lang.Runnable
+  class C extends Runnable { def run() = () }
+
+package p3:
+  import java.lang.Runnable
+  class C extends Runnable { def run() = () }
+
+package p4:
+  import java.lang.{Runnable, System}, System.out
+  class C extends Runnable { def run() = out.println() }
+
+package p5:
+  import java.lang.{Runnable, System}, System.out
+  class C extends Runnable { def run() = out.println() }
+
+package p6:
+  import java.lang.{Runnable, System}, System.out
+  class C extends Runnable { def run() = out.println() }
+
+package p7:
+  import java.lang.{Runnable,
+                    System}, System.out
+  class C extends Runnable { def run() = out.println() }
+
+package p8:
+  import java.lang.{Runnable as R, System}, System.out
+  class C extends R { def run() = out.println() }
+
+package p9:
+  import java.lang.{Runnable as R, System,
+                    Thread}
+  class C extends R { def run() = Thread(() => System.out.println()).start() }
+
+package p10:
+  object X:
+    def x = 42
+  class C:
+    import X.* // preserve text, don't rewrite to p10.X.*
+    def c = x
+
+package p11:
+  import collection.mutable, mutable.ListBuffer // warn // warn // not checked :(
+  def buf = ListBuffer.empty[String]
+
+package p12:
+  import java.lang.System, java.lang.Runnable
+  class C extends Runnable { def run() = System.out.println() }
diff --git a/tests/rewrites/unused.scala b/tests/rewrites/unused.scala
new file mode 100644
index 000000000000..85a83c7c0015
--- /dev/null
+++ b/tests/rewrites/unused.scala
@@ -0,0 +1,61 @@
+
+//> using options -Wunused:all
+
+package p1:
+  import java.lang.Runnable
+  import java.lang.{Thread, String}, java.lang.Integer
+  class C extends Runnable { def run() = () }
+
+package p2:
+  import java.lang.Thread
+  import java.lang.String
+  import java.lang.Runnable
+  import java.lang.Integer
+  class C extends Runnable { def run() = () }
+
+package p3:
+  import java.lang.{Runnable, Thread, String}
+  class C extends Runnable { def run() = () }
+
+package p4:
+  import java.lang.{Runnable, System, Thread}, System.out
+  class C extends Runnable { def run() = out.println() }
+
+package p5:
+  import java.lang.{Thread, Runnable, System}, System.out
+  class C extends Runnable { def run() = out.println() }
+
+package p6:
+  import java.lang.{Runnable, System}, java.lang.Thread, System.out
+  class C extends Runnable { def run() = out.println() }
+
+package p7:
+  import java.lang.{Runnable,
+                    Thread,
+                    System}, System.out
+  class C extends Runnable { def run() = out.println() }
+
+package p8:
+  import java.lang.{Runnable as R, System,
+                    Thread}, System.out
+  class C extends R { def run() = out.println() }
+
+package p9:
+  import java.lang.{Runnable as R, System,
+                    Thread}, System.out
+  class C extends R { def run() = Thread(() => System.out.println()).start() }
+
+package p10:
+  object X:
+    def x = 42
+  class C:
+    import X.*, java.util.HashMap // preserve text, don't rewrite to p10.X.*
+    def c = x
+
+package p11:
+  import collection.mutable, mutable.ListBuffer, java.lang.{Runnable, Thread} // warn // warn // not checked :(
+  def buf = ListBuffer.empty[String]
+
+package p12:
+  import collection.mutable, java.lang.System, java.lang.Runnable
+  class C extends Runnable { def run() = System.out.println() }
diff --git a/tests/semanticdb/metac.expect b/tests/semanticdb/metac.expect
index 7153edac00ab..a89da0fb8aec 100644
--- a/tests/semanticdb/metac.expect
+++ b/tests/semanticdb/metac.expect
@@ -8,7 +8,6 @@ Text => empty
 Language => Scala
 Symbols => 9 entries
 Occurrences => 19 entries
-Diagnostics => 2 entries
 
 Symbols:
 example/Access# => class Access extends Object { self: Access => +8 decls }
@@ -42,10 +41,6 @@ Occurrences:
 [9:6..9:8): m7 <- example/Access#m7().
 [9:11..9:14): ??? -> scala/Predef.`???`().
 
-Diagnostics:
-[3:14..3:16): [warning] unused private member
-[4:20..4:22): [warning] unused private member
-
 expect/Advanced.scala
 ---------------------
 
@@ -559,7 +554,7 @@ Text => empty
 Language => Scala
 Symbols => 108 entries
 Occurrences => 127 entries
-Diagnostics => 4 entries
+Diagnostics => 9 entries
 Synthetics => 2 entries
 
 Symbols:
@@ -802,10 +797,15 @@ Occurrences:
 [53:10..53:11): + -> scala/Int#`+`(+4).
 
 Diagnostics:
+[13:20..13:21): [warning] unused explicit parameter
 [18:9..18:10): [warning] unused explicit parameter
 [20:27..20:28): [warning] unused explicit parameter
 [22:27..22:28): [warning] unused explicit parameter
 [24:10..24:11): [warning] unused explicit parameter
+[36:11..36:12): [warning] unused explicit parameter
+[39:11..39:12): [warning] unused explicit parameter
+[39:19..39:20): [warning] unused explicit parameter
+[51:30..51:31): [warning] unused explicit parameter
 
 Synthetics:
 [51:16..51:27):List(1).map => *[Int]
@@ -2071,6 +2071,7 @@ Text => empty
 Language => Scala
 Symbols => 45 entries
 Occurrences => 66 entries
+Diagnostics => 1 entries
 Synthetics => 3 entries
 
 Symbols:
@@ -2188,6 +2189,9 @@ Occurrences:
 [41:8..41:17): given_Z_T -> givens/InventedNames$package.given_Z_T().
 [41:18..41:24): String -> scala/Predef.String#
 
+Diagnostics:
+[24:13..24:13): [warning] unused implicit parameter
+
 Synthetics:
 [24:0..24:0): => *(x$1)
 [34:8..34:20):given_Double => *(intValue)
@@ -3492,6 +3496,7 @@ Text => empty
 Language => Scala
 Symbols => 62 entries
 Occurrences => 164 entries
+Diagnostics => 3 entries
 Synthetics => 39 entries
 
 Symbols:
@@ -3724,6 +3729,11 @@ Occurrences:
 [68:18..68:24): impure -> local20
 [68:30..68:31): s -> local16
 
+Diagnostics:
+[19:21..19:22): [warning] unused pattern variable
+[41:4..41:5): [warning] unused pattern variable
+[63:10..63:11): [warning] unused explicit parameter
+
 Synthetics:
 [5:2..5:13):List(1).map => *[Int]
 [5:2..5:6):List => *.apply[Int]
@@ -4209,7 +4219,6 @@ Text => empty
 Language => Scala
 Symbols => 8 entries
 Occurrences => 18 entries
-Diagnostics => 1 entries
 
 Symbols:
 _empty_/Test_depmatch. => final object Test_depmatch extends Object { self: Test_depmatch.type => +4 decls }
@@ -4241,9 +4250,6 @@ Occurrences:
 [6:19..6:20): U -> local0
 [6:24..6:27): ??? -> scala/Predef.`???`().
 
-Diagnostics:
-[6:8..6:9): [warning] unused local definition
-
 expect/example-dir/FileInDir.scala
 ----------------------------------
 
@@ -4562,6 +4568,7 @@ Text => empty
 Language => Scala
 Symbols => 24 entries
 Occurrences => 63 entries
+Diagnostics => 2 entries
 
 Symbols:
 _empty_/Copy# => trait Copy [typeparam In  <: Txn[In], typeparam Out  <: Txn[Out]] extends Object { self: Copy[In, Out] => +5 decls }
@@ -4654,6 +4661,10 @@ Occurrences:
 [14:8..14:15): println -> scala/Predef.println(+1).
 [17:4..17:7): out -> local0
 
+Diagnostics:
+[13:12..13:17): [warning] unused pattern variable
+[13:28..13:34): [warning] unused pattern variable
+
 expect/inlineconsume.scala
 --------------------------
 
@@ -4948,7 +4959,7 @@ Text => empty
 Language => Scala
 Symbols => 50 entries
 Occurrences => 78 entries
-Diagnostics => 4 entries
+Diagnostics => 6 entries
 Synthetics => 2 entries
 
 Symbols:
@@ -5088,6 +5099,8 @@ Diagnostics:
 [9:36..9:37): [warning] unused explicit parameter
 [9:42..9:43): [warning] unused explicit parameter
 [21:11..21:12): [warning] unused explicit parameter
+[24:24..24:27): [warning] unused pattern variable
+[25:27..25:28): [warning] unused pattern variable
 
 Synthetics:
 [23:6..23:10):List => *.unapplySeq[Nothing]
@@ -5500,7 +5513,7 @@ Occurrences:
 [119:39..119:42): Int -> scala/Int#
 
 Diagnostics:
-[5:13..5:14): [warning] unused explicit parameter
+[96:13..96:14): [warning] unused explicit parameter
 
 Synthetics:
 [68:20..68:24):@ann => *[Int]
diff --git a/tests/untried/neg/warn-unused-privates.scala b/tests/untried/neg/warn-unused-privates.scala
deleted file mode 100644
index 64e7679f37ac..000000000000
--- a/tests/untried/neg/warn-unused-privates.scala
+++ /dev/null
@@ -1,105 +0,0 @@
-class Bippy(a: Int, b: Int) {
-  private def this(c: Int) = this(c, c)           // warn
-  private def bippy(x: Int): Int      = bippy(x)  // TODO: could warn
-  private def boop(x: Int)            = x + a + b     // warn
-  final private val MILLIS1           = 2000      // no warn, might have been inlined
-  final private val MILLIS2: Int      = 1000      // warn
-  final private val HI_COMPANION: Int = 500       // no warn, accessed from companion
-  def hi() = Bippy.HI_INSTANCE
-}
-object Bippy {
-  def hi(x: Bippy) = x.HI_COMPANION
-  private val HI_INSTANCE: Int = 500      // no warn, accessed from instance
-  private val HEY_INSTANCE: Int = 1000    // warn
-}
-
-class A(val msg: String)
-class B1(msg: String) extends A(msg)
-class B2(msg0: String) extends A(msg0)
-class B3(msg0: String) extends A("msg")
-
-/*** Early defs warnings disabled primarily due to SI-6595.
- *   The test case is here to assure we aren't issuing false positives;
- *   the ones labeled "warn" don't warn.
- ***/
-class Boppy extends {
-  private val hmm: String = "abc"       // no warn, used in early defs
-  private val hom: String = "def"       // no warn, used in body
-  private final val him   = "ghi"       // no warn, might have been (was) inlined
-  final val him2          = "ghi"       // no warn, same
-  final val himinline     = him
-  private val hum: String = "jkl"       // warn
-  final val ding = hmm.length
-} with Mutable {
-  val dinger = hom
-  private val hummer = "def" // warn
-
-  private final val bum   = "ghi"       // no warn, might have been (was) inlined
-  final val bum2          = "ghi"       // no warn, same
-}
-
-trait Accessors {
-  private var v1: Int = 0 // warn
-  private var v2: Int = 0 // warn, never set
-  private var v3: Int = 0 // warn, never got
-  private var v4: Int = 0 // no warn
-
-  def bippy(): Int = {
-    v3 = 5
-    v4 = 6
-    v2 + v4
-  }
-}
-
-trait DefaultArgs {
-  // warn about default getters for x2 and x3
-  private def bippy(x1: Int, x2: Int = 10, x3: Int = 15): Int = x1 + x2 + x3
-
-  def boppy() = bippy(5, 100, 200)
-}
-
-class Outer {
-  class Inner
-}
-
-trait Locals {
-  def f0 = {
-    var x = 1 // warn
-    var y = 2
-    y = 3
-    y + y
-  }
-  def f1 = {
-    val a = new Outer // no warn
-    val b = new Outer // warn
-    new a.Inner
-  }
-  def f2 = {
-    var x = 100 // warn about it being a var
-    x
-  }
-}
-
-object Types {
-  private object Dongo { def f = this } // warn
-  private class Bar1 // warn
-  private class Bar2 // no warn
-  private type Alias1 = String // warn
-  private type Alias2 = String // no warn
-  def bippo = (new Bar2).toString
-
-  def f(x: Alias2) = x.length
-
-  def l1() = {
-    object HiObject { def f = this } // warn
-    class Hi { // warn
-      def f1: Hi = new Hi
-      def f2(x: Hi) = x
-    }
-    class DingDongDoobie // warn
-    class Bippy // no warn
-    type Something = Bippy // no warn
-    type OtherThing = String // warn
-    (new Bippy): Something
-  }
-}
diff --git a/tests/pos/i15226.scala b/tests/warn/i15226.scala
similarity index 100%
rename from tests/pos/i15226.scala
rename to tests/warn/i15226.scala
diff --git a/tests/warn/i15503a.scala b/tests/warn/i15503a.scala
index df8691c21a13..40b6c75983bf 100644
--- a/tests/warn/i15503a.scala
+++ b/tests/warn/i15503a.scala
@@ -1,5 +1,4 @@
-//> using options  -Wunused:imports
-
+//> using options -Wunused:imports -Wconf:origin=Suppressed.*:s
 
 object FooUnused:
   import collection.mutable.Set // warn
@@ -68,7 +67,7 @@ object InlineChecks:
     inline def getSet = Set(1)
 
   object InlinedBar:
-    import collection.mutable.Set // ok
+    import collection.mutable.Set // warn (don't be fooled by inline expansion)
     import collection.mutable.Map // warn
     val a = InlineFoo.getSet
 
@@ -100,20 +99,28 @@ object SomeGivenImports:
   given String = "foo"
 
 /* BEGIN : Check on packages*/
-package testsamepackageimport:
-  package p {
+package nestedpackageimport:
+  package p:
     class C
-  }
-
-  package p {
-    import p._ // warn
-    package q {
-      class U {
+  package p:
+    package q:
+      import p.* // warn
+      class U:
         def f = new C
-      }
-    }
-  }
-// -----------------------
+package unnestedpackageimport:
+  package p:
+    class C
+  package p.q:
+    import p.* // nowarn
+    class U:
+      def f = new C
+
+package redundancy:
+  object redundant:
+    def f = 42
+  import redundancy.* // warn superseded by def in scope
+  class R:
+    def g = redundant.f
 
 package testpackageimport:
   package a:
@@ -213,7 +220,8 @@ package testImportsInImports:
   package c:
     import a.b // OK
     import b.x // OK
-    val y = x
+    import b.x as z // OK
+    val y = x + z
 
 //-------------------------------------
 package testOnOverloadedMethodsImports:
@@ -265,4 +273,51 @@ package foo.test.typeapply.hklamdba.i16680:
     import foo.IO // OK
 
     def f[F[_]]: String = "hello"
-    def go = f[IO]
\ No newline at end of file
+    def go = f[IO]
+
+object Selections:
+  def f(list: List[Int]): Int =
+    import list.{head => first} // OK
+    first
+
+  def f2(list: List[Int]): Int =
+    import list.head // OK
+    head
+
+  def f3(list: List[Int]): Int =
+    import list.head // warn
+    list.head
+
+  object N:
+    val ns: List[Int] = Nil
+
+  def g(): Int =
+    import N.ns // OK
+    ns.head
+end Selections
+
+object `more nestings`:
+  object Outer:
+    object Inner:
+      val thing = 42
+      def j() =
+        import Inner.thing // warn
+        thing
+      def k() =
+        import Inner.thing // warn
+        Inner.thing
+
+  object Thing:
+    object Inner:
+      val thing = 42
+      import Inner.thing // warn
+      def j() =
+        thing
+      def k() =
+        Inner.thing
+
+object Suppressed:
+  val suppressed = 42
+object Suppressing:
+  import Suppressed.* // no warn, see options
+  def f = 42
diff --git a/tests/warn/i15503b.scala b/tests/warn/i15503b.scala
index 7ab86026ff00..0ea110f833f1 100644
--- a/tests/warn/i15503b.scala
+++ b/tests/warn/i15503b.scala
@@ -117,7 +117,7 @@ package foo.scala2.tests:
 
   object Types {
     def l1() = {
-      object HiObject { def f = this } // OK
+      object HiObject { def f = this } // warn
       class Hi { // warn
         def f1: Hi = new Hi
         def f2(x: Hi) = x
@@ -141,4 +141,4 @@ package test.foo.twisted.i16682:
     }
     isInt
 
-  def f = myPackage("42")
\ No newline at end of file
+  def f = myPackage("42")
diff --git a/tests/warn/i15503c.scala b/tests/warn/i15503c.scala
index a813329da89b..86b972487e17 100644
--- a/tests/warn/i15503c.scala
+++ b/tests/warn/i15503c.scala
@@ -1,4 +1,4 @@
-//> using options  -Wunused:privates -source:3.3
+//> using options -Wunused:privates -source:3.3
 
 trait C
 class A:
@@ -33,17 +33,22 @@ class A:
     var w = 2 // OK
 
 package foo.test.constructors:
-  case class A private (x:Int) // OK
+  case class A private (x: Int) // OK
   class B private (val x: Int) // OK
-  class C private (private val x: Int) // warn
+  object B { def default = B(42) }
+  class C private (private val xy: Int) // warn
+  object C { def default = C(42) }
   class D private (private val x: Int): // OK
     def y = x
+  object D { def default = D(42) }
   class E private (private var x: Int): // warn not set
     def y = x
+  object E { def default = E(42) }
   class F private (private var x: Int): // OK
     def y =
       x = 3
       x
+  object F { def default = F(42) }
 
 package test.foo.i16682:
   object myPackage:
@@ -55,4 +60,13 @@ package test.foo.i16682:
         case _ => println("NaN")
     }
 
-  def f = myPackage.isInt("42")
\ No newline at end of file
+  def f = myPackage.isInt("42")
+
+object LazyVals:
+  import java.util.concurrent.CountDownLatch
+
+  // This trait extends Serializable to fix #16806 that caused a race condition
+  sealed trait LazyValControlState extends Serializable
+
+  final class Waiting extends CountDownLatch(1), LazyValControlState:
+    private def writeReplace(): Any = null
diff --git a/tests/warn/i15503d.scala b/tests/warn/i15503d.scala
index 9a0ba9b2f8dc..494952e2e4b0 100644
--- a/tests/warn/i15503d.scala
+++ b/tests/warn/i15503d.scala
@@ -1,5 +1,6 @@
-//> using options  -Wunused:unsafe-warn-patvars
-// todo : change to :patvars
+//> using options -Wunused:patvars
+
+import scala.reflect.Typeable
 
 sealed trait Calc
 sealed trait Const extends Calc
@@ -8,23 +9,118 @@ case class S(pred: Const) extends Const
 case object Z extends Const
 
 val a = Sum(S(S(Z)),Z) match {
-  case Sum(a,Z) => Z // warn
+  case Sum(x,Z) => Z // warn
   // case Sum(a @ _,Z) => Z // todo : this should pass in the future
-  case Sum(a@S(_),Z) => Z // warn
-  case Sum(a@S(_),Z) => a // warn unreachable
-  case Sum(a@S(b@S(_)), Z) => a // warn
-  case Sum(a@S(b@S(_)), Z) => a // warn
-  case Sum(a@S(b@(S(_))), Z) => Sum(a,b) // warn unreachable
+  case Sum(x@S(_),Z) => Z // warn
+  case Sum(x@S(_),Z) => x // warn unreachable
+  case Sum(x@S(y@S(_)), Z) => x // warn
+  case Sum(x@S(y@S(_)), Z) => x // warn
+  case Sum(x@S(y@(S(_))), Z) => Sum(x,y) // warn unreachable
   case Sum(_,_) => Z // OK
   case _ => Z // warn unreachable
 }
 
-// todo : This should pass in the future
-// val b = for {
-//   case Some(x) <- Option(Option(1))
-// } println(s"$x")
+case class K(i: Int, j: Int)
+
+class C(c0: Option[Int], k0: K):
+  private val Some(c) = c0: @unchecked  // warn valdef from pattern
+  private val K(i, j) = k0              // warn // warn valdefs from pattern (RHS patvars are NoWarn)
+  val K(v, w) = k0                      // nowarn nonprivate
+  private val K(r, s) = k0              // warn // warn valdefs from pattern
+  def f(x: Option[Int]) = x match
+    case Some(y) => true                // warn Bind in pattern
+    case _       => false
+  def g(ns: List[Int]) =
+    for x <- ns do println()            // warn valdef function param from for
+  def g1(ns: List[Int]) =
+    for x <- ns do println(x)           // x => println(x)
+  def h(ns: List[Int]) =
+    for x <- ns; y = x + 1              // warn tupling from for; x is used, y is unused
+    do println()
+  def k(x: Option[K]) =
+    x match
+    case Some(K(i, j)) =>               // nowarn canonical names
+    case _ =>
+
+  val m = Map(
+    "first" -> Map((true, 1), (false, 2), (true, 3)),
+    "second" -> Map((true, 1), (false, 2), (true, 3)),
+  )
+  def guardedUse =
+    m.map: (a, m1) =>
+      for (status, lag) <- m1 if status
+      yield (a, status, lag)
+  def guardedUseOnly =
+    m.map: (a, m1) =>
+      for (status, lag) <- m1 if status
+      yield (a, lag)
+  def guardedUseMissing =
+    m.map: (a, m1) =>
+      for (status, lag) <- m1           // warn
+      yield (a, lag)
+  def flatGuardedUse =
+    for (a, m1) <- m; (status, lag) <- m1 if status
+    yield (a, status, lag)
+  def leading =
+    for _ <- List("42"); i = 1; _ <- List("0", "27")(i)
+    yield ()
+  def optional =
+    for case Some(x) <- List(Option(42))
+    yield x
+  def nonoptional =
+    for case Some(x) <- List(Option(42))  // warn
+    yield 27
+  def optionalName =
+    for case Some(value) <- List(Option(42))
+    yield 27
+
+    /*
+  def tester[A](a: A)(using Typeable[K]) =
+    a match
+    case S(i, j) => i + j
+    case _ => 0
+    */
+
+class Wild:
+  def f(x: Any) =
+    x match
+    case _: Option[?] => true
+    case _ => false
+
+def untuple(t: Tuple) =
+  t match
+  case Tuple() =>
+  case h *: t => // no warn canonical names taken from tuple element types, (Head, Tail) -> (head, tail)
+  //case head *: tail => // no warn canonical names taken from tuple element types, (Head, Tail) -> (head, tail)
+
+// empty case class:
+// def equals(other) = other match { case other => true } // exonerated name
+object i15967:
+  sealed trait A[-Z]
+  final case class B[Y]() extends A[Y]
+
+object `patvar is assignable`:
+  var (i, j) = (42, 27) // no warn nonprivate
+  j += 1
+  println((i, j))
+
+object `privy patvar is assignable`:
+  private var (i, j) = (42, 27) // warn
+  j += 1
+  println((i, j))
+
+object `local patvar is assignable`:
+  def f() =
+    var (i, j) = (42, 27) // warn
+    j += 1
+    println((i, j))
+
+object `mutable patvar in for`:
+  def f(xs: List[Int]) =
+    for x <- xs; y = x + 1 if y > 10 yield
+      var z :: Nil = y :: Nil: @unchecked // warn
+      z + 10
 
-// todo : This should *NOT* pass in the future
-// val c = for {
-//   case Some(x) <- Option(Option(1))
-// } println(s"hello world")
\ No newline at end of file
+class `unset var requires -Wunused`:
+  private var i = 0 // no warn as we didn't ask for it
+  def f = println(i)
diff --git a/tests/warn/i15503e.scala b/tests/warn/i15503e.scala
index 46d73a4945cd..2fafec339ac1 100644
--- a/tests/warn/i15503e.scala
+++ b/tests/warn/i15503e.scala
@@ -1,4 +1,6 @@
-//> using options  -Wunused:explicits
+//> using options -Wunused:explicits
+
+import annotation.*
 
 object Foo {
   /* This goes around the "trivial method" detection */
@@ -31,16 +33,17 @@ package scala3main:
 package foo.test.lambda.param:
   val default_val = 1
   val a = (i: Int) => i // OK
-  val b = (i: Int) => default_val // OK
+  val b = (i: Int) => default_val // warn
   val c = (_: Int) => default_val // OK
 
 package foo.test.trivial:
   /* A twisted test from Scala 2 */
-  class C {
+  class C(val value: Int) {
     def answer: 42 = 42
     object X
     private def g0(x: Int) = ??? // OK
     private def f0(x: Int) = () // OK
+    private def f00(x: Int) = {} // OK
     private def f1(x: Int) = throw new RuntimeException // OK
     private def f2(x: Int) = 42 // OK
     private def f3(x: Int): Option[Int] = None // OK
@@ -50,13 +53,16 @@ package foo.test.trivial:
     private def f7(x: Int) = Y // OK
     private def f8(x: Int): List[C] = Nil // OK
     private def f9(x: Int): List[Int] = List(1,2,3,4) // warn
-    private def foo:Int = 32  // OK
+    private def foo: Int = 32  // OK
     private def f77(x: Int) = foo // warn
+    private def self(x: Int): C = this // no warn
+    private def unwrap(x: Int): Int = value // no warn
   }
   object Y
 
 package foo.test.i16955:
-  class S(var r: String) // OK
+  class S(var rrr: String) // OK
+  class T(rrr: String) // warn
 
 package foo.test.i16865:
   trait Foo:
@@ -64,7 +70,26 @@ package foo.test.i16865:
   trait Bar extends Foo
 
   object Ex extends Bar:
-    def fn(a: Int, b: Int): Int = b + 3 // OK
+    def fn(a: Int, b: Int): Int = b + 3 // warn
 
   object Ex2 extends Bar:
-    override def fn(a: Int, b: Int): Int = b + 3 // OK
\ No newline at end of file
+    override def fn(a: Int, b: Int): Int = b + 3 // warn
+
+final class alpha(externalName: String) extends StaticAnnotation // no warn annotation arg
+
+object Unimplemented:
+  import compiletime.*
+  inline def f(inline x: Int | Double): Unit = error("unimplemented") // no warn param of trivial method
+
+def `trivially wrapped`(x: String): String ?=> String = "hello, world" // no warn param of trivial method
+
+object UnwrapTyped:
+  import compiletime.error
+  inline def requireConst(inline x: Boolean | Byte | Short | Int | Long | Float | Double | Char | String): Unit =
+    error("Compiler bug: `requireConst` was not evaluated by the compiler")
+
+  transparent inline def codeOf(arg: Any): String =
+    error("Compiler bug: `codeOf` was not evaluated by the compiler")
+
+object `default usage`:
+  def f(i: Int)(j: Int = i * 2) = j // warn I guess
diff --git a/tests/warn/i15503f.scala b/tests/warn/i15503f.scala
index ccf0b7e74065..9d464bbc1973 100644
--- a/tests/warn/i15503f.scala
+++ b/tests/warn/i15503f.scala
@@ -6,9 +6,40 @@ val default_int = 1
 object Xd {
   private def f1(a: Int) = a // OK
   private def f2(a: Int) = 1 // OK
-  private def f3(a: Int)(using Int) = a // OK
-  private def f4(a: Int)(using Int) = default_int // OK
+  private def f3(a: Int)(using Int) = a // warn
+  private def f4(a: Int)(using Int) = default_int // warn
   private def f6(a: Int)(using Int) = summon[Int] // OK
   private def f7(a: Int)(using Int) = summon[Int] + a // OK
   private def f8(a: Int)(using foo: Int) = a // warn
+  private def f9(a: Int)(using Int) = ??? // OK trivial
+  private def g1(a: Int)(implicit foo: Int) = a // warn
 }
+
+trait T
+object T:
+  def hole(using T) = ()
+
+class C(using T) // warn
+
+class D(using T):
+  def t = T.hole // nowarn
+
+object Example:
+  import scala.quoted.*
+  given OptionFromExpr[T](using Type[T], FromExpr[T]): FromExpr[Option[T]] with
+    def unapply(x: Expr[Option[T]])(using Quotes) = x match
+      case '{ Option[T](${Expr(y)}) } => Some(Option(y))
+      case '{ None } => Some(None)
+      case '{ ${Expr(opt)} : Some[T] } => Some(opt)
+      case _ => None
+
+//absolving names on matches of quote trees requires consulting non-abstract types in QuotesImpl
+object Unmatched:
+  import scala.quoted.*
+  def transform[T](e: Expr[T])(using Quotes): Expr[T] =
+    import quotes.reflect.*
+    def f(tree: Tree) =
+      tree match
+      case Ident(name) =>
+      case _ =>
+    e
diff --git a/tests/warn/i15503g.scala b/tests/warn/i15503g.scala
index fbd9f3c1352c..cfbfcdb04d1e 100644
--- a/tests/warn/i15503g.scala
+++ b/tests/warn/i15503g.scala
@@ -6,8 +6,8 @@ object Foo {
 
   private def f1(a: Int) = a // OK
   private def f2(a: Int) = default_int // warn
-  private def f3(a: Int)(using Int) = a // OK
-  private def f4(a: Int)(using Int) = default_int // warn
+  private def f3(a: Int)(using Int) = a // warn
+  private def f4(a: Int)(using Int) = default_int // warn // warn
   private def f6(a: Int)(using Int) = summon[Int] // warn
   private def f7(a: Int)(using Int) = summon[Int] + a // OK
   /* --- Trivial method check --- */
@@ -20,4 +20,5 @@ package foo.test.i17101:
   extension[A] (x: Test[A]) { // OK
     def value: A = x
     def causesIssue: Unit = println("oh no")
-  }
\ No newline at end of file
+    def isAnIssue(y: A): Boolean = x == x // warn
+  }
diff --git a/tests/warn/i15503h.scala b/tests/warn/i15503h.scala
index 854693981488..a2bf47c843dd 100644
--- a/tests/warn/i15503h.scala
+++ b/tests/warn/i15503h.scala
@@ -1,4 +1,4 @@
-//> using options  -Wunused:linted
+//> using options -Wunused:linted
 
 import collection.mutable.Set // warn
 
@@ -7,7 +7,7 @@ class A {
   val b = 2 // OK
 
   private def c = 2 // warn
-  def d(using x:Int): Int = b // ok
+  def d(using x: Int): Int = b // warn
   def e(x: Int) = 1 // OK
   def f =
     val x = 1 // warn
@@ -15,6 +15,6 @@ class A {
     3
 
   def g(x: Int): Int = x match
-    case x:1 => 0 // OK
+    case x: 1 => 0 // OK
     case _ => 1
-}
\ No newline at end of file
+}
diff --git a/tests/warn/i15503i.scala b/tests/warn/i15503i.scala
index b7981e0e4206..89ff382eb68c 100644
--- a/tests/warn/i15503i.scala
+++ b/tests/warn/i15503i.scala
@@ -1,4 +1,4 @@
-//> using options  -Wunused:all
+//> using options -Wunused:all -Ystop-after:checkUnusedPostInlining
 
 import collection.mutable.{Map => MutMap} // warn
 import collection.mutable.Set // warn
@@ -17,10 +17,12 @@ class A {
   private def c2 = 2 // OK
   def c3 = c2
 
-  def d1(using x:Int): Int = default_int // ok
-  def d2(using x:Int): Int = x // OK
+  def d1(using x: Int): Int = default_int // warn param
+  def d2(using x: Int): Int = x // OK
+  def d3(using Int): Int = summon[Int] // OK
+  def d4(using Int): Int = default_int // warn
 
-  def e1(x: Int) = default_int // ok
+  def e1(x: Int) = default_int // warn param
   def e2(x: Int) = x // OK
   def f =
     val x = 1 // warn
@@ -29,11 +31,15 @@ class A {
     def g = 4 // OK
     y + g
 
-  // todo : uncomment once patvars is fixed
-  // def g(x: Int): Int = x match
-  //   case x:1 => 0 // ?error
-  //   case x:2 => x // ?OK
-  //   case _ => 1 // ?OK
+  def g(x: Int): Int = x match
+    case x: 1 => 0 // no warn same name as selector (for shadowing or unused)
+    case x: 2 => x // OK
+    case _    => 1 // OK
+
+  def h(x: Int): Int = x match
+    case y: 1 => 0 // warn unused despite trivial type and RHS
+    case y: 2 => y // OK
+    case _    => 1 // OK
 }
 
 /* ---- CHECK scala.annotation.unused ---- */
@@ -44,7 +50,7 @@ package foo.test.scala.annotation:
   val default_int = 12
 
   def a1(a: Int) = a // OK
-  def a2(a: Int) = default_int // ok
+  def a2(a: Int) = default_int // warn
 
   def a3(@unused a: Int) = default_int //OK
 
@@ -82,8 +88,8 @@ package foo.test.i16678:
   def run =
     println(foo(number => number.toString, value = 5)) // OK
     println(foo(number => "<number>", value = 5)) // warn
-    println(foo(func = number => "<number>", value = 5)) // warn
     println(foo(func = number => number.toString, value = 5)) // OK
+    println(foo(func = number => "<number>", value = 5)) // warn
     println(foo(func = _.toString, value = 5)) // OK
 
 package foo.test.possibleclasses:
@@ -91,7 +97,7 @@ package foo.test.possibleclasses:
     k: Int, // OK
     private val y: Int // OK /* Kept as it can be taken from pattern */
   )(
-    s: Int,
+    s: Int, // warn
     val t: Int, // OK
     private val z: Int // warn
   )
@@ -130,22 +136,22 @@ package foo.test.possibleclasses:
 package foo.test.possibleclasses.withvar:
   case class AllCaseClass(
     k: Int, // OK
-    private var y: Int // OK /* Kept as it can be taken from pattern */
+    private var y: Int // warn unset
   )(
-    s: Int,
-    var t: Int, // OK
-    private var z: Int // warn
+    s: Int, // warn
+    var ttt: Int, // OK
+    private var zzz: Int // warn
   )
 
   case class AllCaseUsed(
     k: Int, // OK
-    private var y: Int // OK
+    private var y: Int // warn unset
   )(
     s: Int, // OK
-    var t: Int, // OK global scope can be set somewhere else
-    private var z: Int // warn not set
+    var tt: Int, // OK global scope can be set somewhere else
+    private var zz: Int // warn not set
   ) {
-    def a = k + y + s + t + z
+    def a = k + y + s + tt + zz
   }
 
   class AllClass(
@@ -199,14 +205,14 @@ package foo.test.i16877:
 package foo.test.i16926:
   def hello(): Unit =
     for {
-      i <- (0 to 10).toList
+      i <- (0 to 10).toList // warn patvar
       (a, b) = "hello" -> "world" // OK
     } yield println(s"$a $b")
 
 package foo.test.i16925:
   def hello =
     for {
-      i <- 1 to 2 if true
+      i <- 1 to 2 if true // OK
       _ = println(i) // OK
     } yield ()
 
@@ -247,7 +253,7 @@ package foo.test.i16679a:
       import scala.deriving.Mirror
       object CaseClassByStringName:
         inline final def derived[A](using inline A: Mirror.Of[A]): CaseClassByStringName[A] =
-          new CaseClassByStringName[A]: // warn
+          new CaseClassByStringName[A]:
             def name: String = A.toString
 
   object secondPackage:
@@ -263,7 +269,7 @@ package foo.test.i16679b:
     object CaseClassName:
       import scala.deriving.Mirror
       inline final def derived[A](using inline A: Mirror.Of[A]): CaseClassName[A] =
-        new CaseClassName[A]: // warn
+        new CaseClassName[A]:
           def name: String = A.toString
 
   object Foo:
@@ -279,7 +285,7 @@ package foo.test.i17156:
   package a:
     trait Foo[A]
     object Foo:
-      inline def derived[T]: Foo[T] = new Foo{} // warn
+      inline def derived[T]: Foo[T] = new Foo {}
 
   package b:
     import a.Foo
@@ -313,3 +319,52 @@ package foo.test.i17117:
       val test = t1.test
     }
   }
+
+// manual testing of cached look-ups
+package deeply:
+  object Deep:
+    def f(): Unit =
+      def g(): Unit =
+        def h(): Unit =
+          println(Deep)
+          println(Deep)
+          println(Deep)
+        h()
+      g()
+    override def toString = "man, that is deep!"
+/* result cache saves before context traversal and import/member look-up
+CHK object Deep
+recur * 10 = context depth is 10 between reference and definition
+CHK method println
+recur = was 19 at predef where max root is 21
+CHK object Deep
+recur = cached result
+CHK method println
+recur
+CHK object Deep
+recur
+*/
+
+package constructors:
+  class C private (i: Int): // warn param
+    def this() = this(27)
+    private def this(s: String) = this(s.toInt) // warn ctor
+    def c = new C(42)
+    private def f(i: Int) = i // warn overloaded member
+    private def f(s: String) = s
+    def g = f("hello") // use one of overloaded member
+
+  class D private (i: Int):
+    private def this() = this(42) // no warn used by companion
+    def d = i
+  object D:
+    def apply(): D = new D()
+
+package reversed: // reverse-engineered
+  class C:
+    def c: scala.Int = 42 // Int marked used; lint does not look up <empty>.scala
+  class D:
+    def d: Int = 27 // Int is found in root import scala.*
+  class E:
+    import scala.* // no warn because root import (which is cached! by previous lookup) is lower precedence
+    def e: Int = 27
diff --git a/tests/warn/i15503k.scala b/tests/warn/i15503k.scala
new file mode 100644
index 000000000000..8148de44c588
--- /dev/null
+++ b/tests/warn/i15503k.scala
@@ -0,0 +1,43 @@
+
+//> using options -Wunused:imports
+
+import scala.compiletime.ops.int.* // no warn
+
+object TupleOps:
+  /** Type of the element at position N in the tuple X */
+  type Elem[X <: Tuple, N <: Int] = X match {
+    case x *: xs =>
+      N match {
+        case 0 => x
+        case S[n1] => Elem[xs, n1]
+      }
+  }
+
+  /** Literal constant Int size of a tuple */
+  type Size[X <: Tuple] <: Int = X match {
+    case EmptyTuple => 0
+    case x *: xs => S[Size[xs]]
+  }
+
+object Summoner:
+  transparent inline def summoner[T](using x: T): x.type = x
+
+object `Summoner's Tale`:
+  import compiletime.summonFrom // no warn
+  inline def valueOf[T]: T = summonFrom: // implicit match
+    case ev: ValueOf[T] => ev.value
+  import Summoner.* // no warn
+  def f[T](using T): T = summoner[T] // Inlined
+
+class C:
+  private def m: Int = 42 // no warn
+object C:
+  class D:
+    private val c: C = C() // no warn
+    export c.m // no work to do, expanded member is non-private and uses the select expr
+
+object UsefulTypes:
+  trait T
+object TypeUser:
+  import UsefulTypes.*
+  def f(x: => T) = x
diff --git a/tests/warn/i15503kb/power.scala b/tests/warn/i15503kb/power.scala
new file mode 100644
index 000000000000..f7e5ccce6d58
--- /dev/null
+++ b/tests/warn/i15503kb/power.scala
@@ -0,0 +1,14 @@
+
+object Power:
+  import scala.math.pow as power
+  import scala.quoted.*
+  inline def powerMacro(x: Double, inline n: Int): Double = ${ powerCode('x, 'n) }
+  def powerCode(x: Expr[Double], n: Expr[Int])(using Quotes): Expr[Double] =
+    n match
+    case Expr(m) => unrolledPowerCode(x, m)
+    case _ => '{ power($x, $n.toDouble) }
+  def unrolledPowerCode(x: Expr[Double], n: Int)(using Quotes): Expr[Double] =
+    n match
+    case 0 => '{ 1.0 }
+    case 1 => x
+    case _ => '{ $x * ${ unrolledPowerCode(x, n - 1) } }
diff --git a/tests/warn/i15503kb/square.scala b/tests/warn/i15503kb/square.scala
new file mode 100644
index 000000000000..2a5f76e9be83
--- /dev/null
+++ b/tests/warn/i15503kb/square.scala
@@ -0,0 +1,5 @@
+//> using options -Werror -Wunused:all
+
+object PowerUser:
+  import Power.*
+  def square(x: Double): Double = powerMacro(x, 2)
diff --git a/tests/pos/i15967.scala b/tests/warn/i15967.scala
similarity index 100%
rename from tests/pos/i15967.scala
rename to tests/warn/i15967.scala
diff --git a/tests/warn/i16639a.scala b/tests/warn/i16639a.scala
index 9fe4efe57d7b..ca7798297aea 100644
--- a/tests/warn/i16639a.scala
+++ b/tests/warn/i16639a.scala
@@ -1,7 +1,7 @@
 //> using options  -Wunused:all -source:3.3
 
 class Bippy(a: Int, b: Int) {
-  private def this(c: Int) = this(c, c)
+  private def this(c: Int) = this(c, c) // warn
   private def boop(x: Int)            = x+a+b     // warn
     private def bippy(x: Int): Int      = bippy(x)  // warn TODO: could warn
   final private val MILLIS1           = 2000      // warn no warn, /Dotty:Warn
@@ -24,13 +24,13 @@ class B3(msg0: String) extends A("msg") // warn /Dotty: unused explicit paramete
 trait Bing
 
 trait Accessors {
-  private var v1: Int = 0 // warn warn
-  private var v2: Int = 0 // warn warn, never set
-  private var v3: Int = 0
+  private var v1: Int = 0 // warn
+  private var v2: Int = 0 // warn, never set
+  private var v3: Int = 0 // warn, never got
   private var v4: Int = 0 // no warn
 
-  private[this] var v5 = 0 // warn warn, never set
-  private[this] var v6 = 0
+  private[this] var v5 = 0 // warn, never set
+  private[this] var v6 = 0 // warn, never got
   private[this] var v7 = 0 // no warn
 
   def bippy(): Int = {
@@ -43,13 +43,13 @@ trait Accessors {
 }
 
 class StableAccessors {
-  private var s1: Int = 0 // warn warn
-  private var s2: Int = 0 // warn warn, never set
-  private var s3: Int = 0
+  private var s1: Int = 0 // warn
+  private var s2: Int = 0 // warn, never set
+  private var s3: Int = 0 // warn, never got
   private var s4: Int = 0 // no warn
 
-  private[this] var s5 = 0 // warn warn, never set
-  private[this] var s6 = 0 // no warn, limitation /Dotty: Why limitation ?
+  private[this] var s5 = 0 // warn, never set
+  private[this] var s6 = 0 // warn, never got
   private[this] var s7 = 0 // no warn
 
   def bippy(): Int = {
@@ -62,7 +62,7 @@ class StableAccessors {
 }
 
 trait DefaultArgs {
-  private def bippy(x1: Int, x2: Int = 10, x3: Int = 15): Int = x1 + x2 + x3 // no more warn warn since #17061
+  private def bippy(x1: Int, x2: Int = 10, x3: Int = 15): Int = x1 + x2 + x3 // warn // warn
 
   def boppy() = bippy(5, 100, 200)
 }
@@ -91,7 +91,7 @@ trait Locals {
 }
 
 object Types {
-  private object Dongo { def f = this } // no more warn since #17061
+  private object Dongo { def f = this } // warn
   private class Bar1 // warn warn
   private class Bar2 // no warn
   private type Alias1 = String // warn warn
@@ -101,7 +101,7 @@ object Types {
   def f(x: Alias2) = x.length
 
   def l1() = {
-    object HiObject { def f = this } // no more warn since #17061
+    object HiObject { def f = this } // warn
     class Hi { // warn warn
       def f1: Hi = new Hi
       def f2(x: Hi) = x
@@ -124,9 +124,9 @@ trait Underwarn {
 }
 
 class OtherNames {
-  private def x_=(i: Int): Unit = () // no more warn since #17061
-  private def x: Int = 42 // warn Dotty triggers unused private member : To investigate
-  private def y_=(i: Int): Unit = () // // no more warn since #17061
+  private def x_=(i: Int): Unit = () // warn
+  private def x: Int = 42 // warn
+  private def y_=(i: Int): Unit = () // warn
   private def y: Int = 42
 
   def f = y
@@ -145,7 +145,7 @@ trait Forever {
     val t = Option((17, 42))
     for {
       ns <- t
-      (i, j) = ns                        // no warn
+      (i, j) = ns                        // warn // warn -Wunused:patvars is in -Wunused:all
     } yield 42                           // val emitted only if needed, hence nothing unused
   }
 }
@@ -158,14 +158,14 @@ trait CaseyKasem {
   def f = 42 match {
     case x if x < 25 => "no warn"
     case y if toString.nonEmpty => "no warn" + y
-    case z => "warn"
+    case z => "warn" // warn patvar
   }
 }
 trait CaseyAtTheBat {
   def f = Option(42) match {
     case Some(x) if x < 25 => "no warn"
-    case Some(y @ _) if toString.nonEmpty => "no warn"
-    case Some(z) => "warn"
+    case Some(y @ _) if toString.nonEmpty => "no warn" // warn todo whether to use name @ _ to suppress
+    case Some(z) => "warn" // warn patvar
     case None => "no warn"
   }
 }
@@ -173,7 +173,7 @@ trait CaseyAtTheBat {
 class `not even using companion privates`
 
 object `not even using companion privates` {
-  private implicit class `for your eyes only`(i: Int) {  // no more warn since #17061
+  private implicit class `for your eyes only`(i: Int) {  // warn
     def f = i
   }
 }
@@ -202,4 +202,4 @@ trait `short comings` {
     val x = 42 // warn /Dotty only triggers in dotty
     17
   }
-}
\ No newline at end of file
+}
diff --git a/tests/pos/i17230.min1.scala b/tests/warn/i17230.min1.scala
similarity index 100%
rename from tests/pos/i17230.min1.scala
rename to tests/warn/i17230.min1.scala
diff --git a/tests/pos/i17230.orig.scala b/tests/warn/i17230.orig.scala
similarity index 100%
rename from tests/pos/i17230.orig.scala
rename to tests/warn/i17230.orig.scala
diff --git a/tests/pos/i17314.scala b/tests/warn/i17314.scala
similarity index 84%
rename from tests/pos/i17314.scala
rename to tests/warn/i17314.scala
index 8ece4a3bd7ac..cff90d843c38 100644
--- a/tests/pos/i17314.scala
+++ b/tests/warn/i17314.scala
@@ -1,4 +1,4 @@
-//> using options -Xfatal-warnings -Wunused:all -deprecation -feature
+//> using options -Wunused:all -deprecation -feature
 
 import java.net.URI
 
@@ -10,9 +10,7 @@ object circelike {
   type Configuration
   trait ConfiguredCodec[T]
   object ConfiguredCodec:
-    inline final def derived[A](using conf: Configuration)(using
-      inline mirror: Mirror.Of[A]
-    ): ConfiguredCodec[A] =
+    inline final def derived[A](using conf: Configuration)(using inline mirror: Mirror.Of[A]): ConfiguredCodec[A] = // warn // warn
       class InlinedConfiguredCodec extends ConfiguredCodec[A]:
         val codec = summonInline[Codec[URI]] // simplification
       new InlinedConfiguredCodec
diff --git a/tests/pos/i17314a.scala b/tests/warn/i17314a.scala
similarity index 70%
rename from tests/pos/i17314a.scala
rename to tests/warn/i17314a.scala
index 4bce56d8bbed..14ae96848d63 100644
--- a/tests/pos/i17314a.scala
+++ b/tests/warn/i17314a.scala
@@ -1,4 +1,4 @@
-//> using options -Xfatal-warnings -Wunused:all -deprecation -feature
+//> using options -Werror -Wunused:all -deprecation -feature
 
 package foo:
   class Foo[T]
diff --git a/tests/warn/i17314b.scala b/tests/warn/i17314b.scala
index e1500028ca93..ad4c8f1e4a31 100644
--- a/tests/warn/i17314b.scala
+++ b/tests/warn/i17314b.scala
@@ -1,4 +1,4 @@
-//> using options  -Wunused:all
+//> using options -Wunused:all
 
 package foo:
   class Foo[T]
diff --git a/tests/warn/i17318.scala b/tests/warn/i17318.scala
new file mode 100644
index 000000000000..d242c96484a7
--- /dev/null
+++ b/tests/warn/i17318.scala
@@ -0,0 +1,33 @@
+
+//> using options -Werror -Wunused:all
+
+object events {
+  final val PollOut = 0x002
+  transparent inline def POLLIN = 0x001
+}
+
+def withShort(v: Short): Unit = ???
+def withInt(v: Int): Unit = ???
+
+def usage() =
+  import events.POLLIN // reports unused
+  def v: Short = POLLIN
+  println(v)
+
+def usage2() =
+  import events.POLLIN // reports unused
+  withShort(POLLIN)
+
+def usage3() =
+  import events.POLLIN // does not report unused
+  withInt(POLLIN)
+
+def usage4() =
+  import events.POLLIN // reports unused
+  withShort(POLLIN)
+
+def value = 42
+def withDouble(v: Double): Unit = ???
+def usage5() = withDouble(value)
+def usage6() = withShort(events.POLLIN)
+def usage7() = withShort(events.PollOut)
diff --git a/tests/warn/i17371.scala b/tests/warn/i17371.scala
new file mode 100644
index 000000000000..f9be76bfed07
--- /dev/null
+++ b/tests/warn/i17371.scala
@@ -0,0 +1,39 @@
+//> using options -Wunused:all
+
+class A
+class B
+
+def Test() =
+  val ordA: Ordering[A] = ???
+  val ordB: Ordering[B] = ???
+  val a: A = ???
+  val b: B = ???
+
+  import ordA.given
+  val _ = a > a
+
+  import ordB.given
+  val _ = b < b
+
+// unminimized OP
+trait Circular[T] extends Ordering[T]
+trait Turns[C: Circular, T] extends Ordering[T]: // warn Circular is not a marker interface
+  extension (turns: T) def extract: C
+
+def f[K, T](start: T, end: T)(using circular: Circular[K], turns: Turns[K, T]): Boolean =
+  import turns.given
+  if start > end then throw new IllegalArgumentException("start must be <= end")
+
+  import circular.given
+  start.extract < end.extract
+
+// -Wunused:implicits warns for unused implicit evidence unless it is an empty interface (only universal members).
+// scala 2 also offers -Wunused:synthetics for whether to warn for synthetic implicit params.
+object ContextBounds:
+  class C[A: Ordered](a: A): // warn
+    def f = a
+
+  trait T[A]
+
+  class D[A: T](a: A): // no warn
+    def f = a
diff --git a/tests/warn/i17667.scala b/tests/warn/i17667.scala
new file mode 100644
index 000000000000..cc3f6bfc8472
--- /dev/null
+++ b/tests/warn/i17667.scala
@@ -0,0 +1,10 @@
+
+//> using options -Wunused:imports
+
+object MyImplicits:
+  extension (a: Int) def print: Unit = println(s"Hello, I am $a")
+
+import MyImplicits.print //Global import of extension
+object Foo:
+  def printInt(a: Int): Unit = a.print
+  import MyImplicits.* // warn //Local import of extension
diff --git a/tests/warn/i17667b.scala b/tests/warn/i17667b.scala
new file mode 100644
index 000000000000..ba8fc7219945
--- /dev/null
+++ b/tests/warn/i17667b.scala
@@ -0,0 +1,22 @@
+
+//> using options -Wunused:all
+
+import scala.util.Try
+import scala.concurrent.* // warn
+import scala.collection.Set
+class C {
+  def ss[A](using Set[A]) = println() // warn
+  private def f = Try(42).get
+  private def z: Int =  // warn
+    Try(27 + z).get
+  def g = f + f
+  def k =
+    val i = g + g
+    val j = i + 2 // warn
+    i + 1
+  def c = C()
+  import scala.util.Try // warn
+}
+class D {
+  def d = C().g
+}
diff --git a/tests/warn/i17753.scala b/tests/warn/i17753.scala
new file mode 100644
index 000000000000..66e4fdb8727b
--- /dev/null
+++ b/tests/warn/i17753.scala
@@ -0,0 +1,10 @@
+//> using options -Wunused:all
+
+class PartiallyApplied[A] {
+  transparent inline def func[B](): Nothing = ???
+}
+
+def call[A] = new PartiallyApplied[A]
+
+def good = call[Int].func[String]() // no warn inline proxy
+def bad = { call[Int].func[String]() } // no warn inline proxy
diff --git a/tests/warn/i18313.scala b/tests/warn/i18313.scala
new file mode 100644
index 000000000000..44b005b313e4
--- /dev/null
+++ b/tests/warn/i18313.scala
@@ -0,0 +1,14 @@
+//> using options -Werror -Wunused:imports
+
+import scala.deriving.Mirror
+
+case class Test(i: Int, d: Double)
+case class Decoder(d: Product => Test)
+
+// OK, no warning returned
+//val ok = Decoder(summon[Mirror.Of[Test]].fromProduct)
+//
+// returns warning:
+// [warn] unused import
+// [warn] import scala.deriving.Mirror
+val d = Decoder(d = summon[Mirror.Of[Test]].fromProduct) // no warn
diff --git a/tests/warn/i18366.scala b/tests/warn/i18366.scala
new file mode 100644
index 000000000000..b6385b5bbb59
--- /dev/null
+++ b/tests/warn/i18366.scala
@@ -0,0 +1,19 @@
+//> using options -Werror -Wunused:all
+
+trait Builder {
+  def foo(): Unit
+}
+
+def `i18366` =
+  val builder: Builder = ???
+  import builder.{foo => bar}
+  bar()
+
+import java.io.DataOutputStream
+
+val buffer: DataOutputStream = ???
+
+import buffer.{write => put}
+
+def `i17315` =
+  put(0: Byte)
diff --git a/tests/warn/i18564.scala b/tests/warn/i18564.scala
new file mode 100644
index 000000000000..19682b7955f9
--- /dev/null
+++ b/tests/warn/i18564.scala
@@ -0,0 +1,39 @@
+
+//> using options -Wunused:imports
+
+import scala.compiletime.*
+import scala.deriving.*
+
+trait Foo
+
+trait HasFoo[A]:
+  /** true if A contains a Foo */
+  val hasFoo: Boolean
+
+// given instances that need to be imported to be in scope
+object HasFooInstances:
+  given defaultHasFoo[A]: HasFoo[A] with
+    val hasFoo: Boolean = false
+  given HasFoo[Foo] with
+    val hasFoo: Boolean = true
+
+object HasFooDeriving:
+
+  inline private def tupleHasFoo[T <: Tuple]: Boolean =
+    inline erasedValue[T] match
+    case _: EmptyTuple => false
+    case _: (t *: ts)  => summonInline[HasFoo[t]].hasFoo || tupleHasFoo[ts]
+
+  inline def deriveHasFoo[T](using p: Mirror.ProductOf[T]): HasFoo[T] =
+    // falsely reported as unused even though it has influence on this code
+    import HasFooInstances.given // no warn at inline method
+    val pHasFoo = tupleHasFoo[p.MirroredElemTypes]
+    new HasFoo[T]: // warn New anonymous class definition will be duplicated at each inline site
+      val hasFoo: Boolean = pHasFoo
+
+/* the import is used upon inline elaboration
+object Test:
+  import HasFooDeriving.*
+  case class C(x: Foo, y: Int)
+  def f: HasFoo[C] = deriveHasFoo[C]
+*/
diff --git a/tests/warn/i19252.scala b/tests/warn/i19252.scala
new file mode 100644
index 000000000000..42ca99208299
--- /dev/null
+++ b/tests/warn/i19252.scala
@@ -0,0 +1,13 @@
+//> using options -Werror -Wunused:all
+object Deps:
+  trait D1
+  object D2
+end Deps
+
+object Bug:
+  import Deps.D1 // no warn
+
+  class Cl(d1: D1):
+    import Deps.*
+    def f = (d1, D2)
+end Bug
diff --git a/tests/warn/i19657-mega.scala b/tests/warn/i19657-mega.scala
new file mode 100644
index 000000000000..eeeb33ab3da7
--- /dev/null
+++ b/tests/warn/i19657-mega.scala
@@ -0,0 +1,4 @@
+//> using options -Xlint:type-parameter-shadow -Wunused:all
+
+class F[X, M[N[X]]]: // warn
+  private def x[X] = toString // warn // warn
diff --git a/tests/warn/i19657.scala b/tests/warn/i19657.scala
new file mode 100644
index 000000000000..2caa1c832abe
--- /dev/null
+++ b/tests/warn/i19657.scala
@@ -0,0 +1,117 @@
+//> using options -Wunused:imports -Ystop-after:checkUnusedPostInlining
+
+trait Schema[A]
+
+case class Foo()
+case class Bar()
+
+trait SchemaGenerator[A] {
+  given Schema[A] = new Schema[A]{}
+}
+
+object FooCodec extends SchemaGenerator[Foo]
+object BarCodec extends SchemaGenerator[Bar]
+
+def summonSchemas(using Schema[Foo], Schema[Bar]) = ()
+
+def summonSchema(using Schema[Foo]) = ()
+
+def `i19657 check prefix to pick selector`: Unit =
+  import FooCodec.given
+  import BarCodec.given
+  summonSchemas
+
+def `i19657 regression test`: Unit =
+  import FooCodec.given
+  import BarCodec.given // warn
+  summonSchema
+
+def `i19657 check prefix to pick specific selector`: Unit =
+  import FooCodec.given_Schema_A
+  import BarCodec.given_Schema_A
+  summonSchemas
+
+def `same symbol different names`: Unit =
+  import FooCodec.given_Schema_A
+  import FooCodec.given_Schema_A as AThing
+  summonSchema(using given_Schema_A)
+  summonSchema(using AThing)
+
+package i17156:
+  package a:
+    trait Foo[A]
+    object Foo:
+      class Food[A] extends Foo[A]
+      inline def derived[T]: Foo[T] = Food()
+
+  package b:
+    import a.Foo
+    type Xd[A] = Foo[A]
+
+  package c:
+    import b.Xd
+    trait Z derives Xd // checks if dealiased import is prefix a.Foo
+    class Bar extends Xd[Int] // checks if import qual b is prefix of b.Xd
+
+object Coll:
+  class C:
+    type HM[K, V] = scala.collection.mutable.HashMap[K, V]
+object CC extends Coll.C
+import CC.*
+
+def `param type is imported`(map: HM[String, String]): Unit = println(map("hello, world"))
+
+object Constants:
+  final val i = 42
+  def extra = 3
+def `old-style constants are usages`: Unit =
+  object Local:
+    final val j = 27
+  import Constants.i
+  println(i + Local.j)
+
+object Constantinople:
+  val k = 42
+class `scope of super`:
+  import Constants.i // was bad warn
+  class C(x: Int):
+    def y = x
+  class D(j: Int) extends C(i + j):
+    import Constants.* // does not resolve i in C(i) and does not shadow named import
+    def m = i // actually picks the higher-precedence import
+    def f =
+      import Constantinople.*
+      class E(e: Int) extends C(i + k):
+        def g = e + y + k + 1
+      E(0).g
+    def consume = extra // use the wildcard import from Constants
+
+import scala.annotation.meta.*
+object Alias {
+  type A = Deprecated @param
+}
+
+// avoid reporting on runtime (nothing to do with transparent inline)
+import scala.runtime.EnumValue
+
+trait Lime
+
+enum Color(val rgb: Int):
+  case Red   extends Color(0xFF0000) with EnumValue
+  case Green extends Color(0x00FF00) with Lime
+  case Blue  extends Color(0x0000FF)
+
+object prefixes:
+  class C:
+    object N:
+      type U
+  object Test:
+    val c: C = ???
+    def k2: c.N.U = ???
+    import c.N.*
+    def k3: U = ??? // TypeTree if not a select
+  object Alt:
+    val c: C = ???
+    import c.N
+    def k4: N.U = ???
+end prefixes
diff --git a/tests/warn/i20520.scala b/tests/warn/i20520.scala
new file mode 100644
index 000000000000..e09d16c27af2
--- /dev/null
+++ b/tests/warn/i20520.scala
@@ -0,0 +1,11 @@
+
+//> using options -Wunused:all
+
+@main def run =
+  val veryUnusedVariable: Int = value // warn local
+
+package i20520:
+  private def veryUnusedMethod(x: Int): Unit = println() // warn param
+  private val veryUnusedVariableToplevel: Unit = println() // package members are accessible under separate compilation
+
+def value = 42
diff --git a/tests/pos/i20860.scala b/tests/warn/i20860.scala
similarity index 74%
rename from tests/pos/i20860.scala
rename to tests/warn/i20860.scala
index 1e1ddea11b75..b318d861fce0 100644
--- a/tests/pos/i20860.scala
+++ b/tests/warn/i20860.scala
@@ -1,3 +1,5 @@
+//> using options -Werror -Wunused:imports
+
 def `i20860 use result to check selector bound`: Unit =
   import Ordering.Implicits.given Ordering[?]
   summon[Ordering[Seq[Int]]]
diff --git a/tests/warn/i20951.scala b/tests/warn/i20951.scala
new file mode 100644
index 000000000000..0ca82e1b8d66
--- /dev/null
+++ b/tests/warn/i20951.scala
@@ -0,0 +1,7 @@
+//> using options -Wunused:all
+object Foo {
+  val dummy = 42
+  def f(): Unit = Option(1).map((x: Int) => dummy) // warn
+  def g(): Unit = Option(1).map((x: Int) => ???) // warn
+  def main(args: Array[String]): Unit = {}
+}
diff --git a/tests/warn/i21420.scala b/tests/warn/i21420.scala
index 0ee4aa3f28f6..65a288c7ae5b 100644
--- a/tests/warn/i21420.scala
+++ b/tests/warn/i21420.scala
@@ -1,4 +1,4 @@
-//> using options -Wunused:imports
+//> using options -Werror -Wunused:imports
 
 object decisions4s{
   trait HKD
@@ -7,7 +7,7 @@ object decisions4s{
 
 object DiagnosticsExample {
   import decisions4s.HKD
-  val _ = new HKD {}
+  case class Input[F[_]]() extends HKD
   import decisions4s.*
-  val _ = new DecisionTable {}
+  val decisionTable: DecisionTable = ???
 }
diff --git a/tests/warn/i21525.scala b/tests/warn/i21525.scala
new file mode 100644
index 000000000000..aa156dc3960e
--- /dev/null
+++ b/tests/warn/i21525.scala
@@ -0,0 +1,20 @@
+//> using options -Werror -Wunused:imports
+
+import scala.reflect.TypeTest
+
+trait A {
+  type B
+  type C <: B
+
+  given instance: TypeTest[B, C]
+}
+
+def f(a: A, b: a.B): Boolean = {
+  import a.C
+  b match {
+    case _: C =>
+      true
+    case _ =>
+      false
+  }
+}
diff --git a/tests/warn/i21809.scala b/tests/warn/i21809.scala
new file mode 100644
index 000000000000..91e7a334b13b
--- /dev/null
+++ b/tests/warn/i21809.scala
@@ -0,0 +1,17 @@
+//> using options -Wunused:imports
+
+package p {
+  package q {
+    import q.* // warn so long as we pass typer
+    class Test {
+      //override def toString = new C().toString + " for Test"
+      def d = D()
+    }
+    class D
+  }
+}
+package q {
+  class C {
+    override def toString = "q.C"
+  }
+}
diff --git a/tests/warn/i21917.scala b/tests/warn/i21917.scala
new file mode 100644
index 000000000000..cade4e90db3d
--- /dev/null
+++ b/tests/warn/i21917.scala
@@ -0,0 +1,27 @@
+//> using options -Wunused:imports
+
+import Pet.Owner
+
+class Dog(owner: Owner) extends Pet(owner) {
+  import Pet.* // warn although unambiguous (i.e., it was disambiguated)
+  //import Car.* // ambiguous
+
+  def bark(): String = "bite"
+
+  def this(owner: Owner, goodDog: Boolean) = {
+    this(owner)
+    if (goodDog) println(s"$owner's dog is a good boy")
+  }
+
+  val getOwner: Owner = owner
+}
+
+class Pet(val owner: Owner)
+
+object Pet {
+  class Owner
+}
+
+object Car {
+  class Owner
+}
diff --git a/tests/warn/i3323.scala b/tests/warn/i3323.scala
new file mode 100644
index 000000000000..e409134ff0ba
--- /dev/null
+++ b/tests/warn/i3323.scala
@@ -0,0 +1,8 @@
+//> using options -Werror
+class Foo {
+  def foo[A](lss: List[List[A]]): Unit = {
+    lss match {
+      case xss: List[List[A]] => // no warn erasure
+    }
+  }
+}
diff --git a/tests/pos/patmat-exhaustive.scala b/tests/warn/patmat-exhaustive.scala
similarity index 56%
rename from tests/pos/patmat-exhaustive.scala
rename to tests/warn/patmat-exhaustive.scala
index 9e3cb7d8f615..a8f057664829 100644
--- a/tests/pos/patmat-exhaustive.scala
+++ b/tests/warn/patmat-exhaustive.scala
@@ -1,4 +1,4 @@
-//> using options -Xfatal-warnings -deprecation -feature
+//> using options -Werror -deprecation -feature
 
 def foo: Unit =
   object O:
@@ -8,5 +8,5 @@ def foo: Unit =
 
   val x: O.A = ???
   x match
-    case x: B => ???
-    case x: C => ???
+  case _: B => ???
+  case _: C => ???
diff --git a/tests/warn/scala2-t11681.scala b/tests/warn/scala2-t11681.scala
index ae2187181ceb..5d752777f64c 100644
--- a/tests/warn/scala2-t11681.scala
+++ b/tests/warn/scala2-t11681.scala
@@ -23,7 +23,7 @@ trait BadAPI extends InterFace {
     a
   }
   override def call(a: Int,
-                    b: String,               // OK
+                    b: String, // warn now
                     c: Double): Int = {
     println(c)
     a
@@ -33,7 +33,7 @@ trait BadAPI extends InterFace {
 
   override def equals(other: Any): Boolean = true  // OK
 
-  def i(implicit s: String) = answer           // ok
+  def i(implicit s: String) = answer // warn now
 
   /*
   def future(x: Int): Int = {
@@ -59,10 +59,10 @@ class Revaluing(u: Int) { def f = u } // OK
 
 case class CaseyKasem(k: Int)        // OK
 
-case class CaseyAtTheBat(k: Int)(s: String)        // ok
+case class CaseyAtTheBat(k: Int)(s: String)        // warn unused s
 
 trait Ignorance {
-  def f(readResolve: Int) = answer           // ok
+  def f(readResolve: Int) = answer  // warn now
 }
 
 class Reusing(u: Int) extends Unusing(u)   // OK
@@ -78,30 +78,30 @@ trait Unimplementation {
 
 trait DumbStuff {
   def f(implicit dummy: DummyImplicit) = answer // ok
-  def g(dummy: DummyImplicit) = answer // ok
+  def g(dummy: DummyImplicit) = answer // warn now
 }
 trait Proofs {
   def f[A, B](implicit ev: A =:= B) = answer // ok
   def g[A, B](implicit ev: A <:< B) = answer // ok
-  def f2[A, B](ev: A =:= B) = answer // ok
-  def g2[A, B](ev: A <:< B) = answer // ok
+  def f2[A, B](ev: A =:= B) = answer // warn now
+  def g2[A, B](ev: A <:< B) = answer // warn now
 }
 
 trait Anonymous {
-  def f = (i: Int) => answer      // ok
+  def f = (i: Int) => answer      // warn now
 
   def f1 = (_: Int) => answer     // OK
 
   def f2: Int => Int = _ + 1  // OK
 
-  def g = for (i <- List(1)) yield answer    // ok
+  def g = for (i <- List(1)) yield answer    // no warn (that is a patvar)
 }
 trait Context[A]
 trait Implicits {
-  def f[A](implicit ctx: Context[A]) = answer // ok
-  def g[A: Context] = answer // OK
+  def f[A](implicit ctx: Context[A]) = answer // warn implicit param even though only marker
+  def g[A: Context] = answer // no warn bound that is marker only
 }
-class Bound[A: Context] // OK
+class Bound[A: Context] // no warn bound that is marker only
 object Answers {
   def answer: Int = 42
 }
diff --git a/tests/warn/tuple-exhaustivity.scala b/tests/warn/tuple-exhaustivity.scala
new file mode 100644
index 000000000000..9060d112b197
--- /dev/null
+++ b/tests/warn/tuple-exhaustivity.scala
@@ -0,0 +1,6 @@
+//> using options -Werror -deprecation -feature
+
+def test(t: Tuple)  =
+  t match
+    case Tuple() =>
+    case h *: t =>
diff --git a/tests/warn/unused-can-equal.scala b/tests/warn/unused-can-equal.scala
new file mode 100644
index 000000000000..6e38591ccef1
--- /dev/null
+++ b/tests/warn/unused-can-equal.scala
@@ -0,0 +1,16 @@
+
+//> using options -Werror -Wunused:all
+
+import scala.language.strictEquality
+
+class Box[T](x: T) derives CanEqual:
+  def y = x
+
+def f[A, B](a: A, b: B)(using CanEqual[A, B]) = a == b // no warn
+
+def g =
+  import Box.given // no warn
+  "42".length
+
+@main def test() = println:
+  Box(1) == Box(1L)
diff --git a/tests/warn/unused-locals.scala b/tests/warn/unused-locals.scala
new file mode 100644
index 000000000000..10bf160fb717
--- /dev/null
+++ b/tests/warn/unused-locals.scala
@@ -0,0 +1,43 @@
+//> using options -Wunused:locals
+
+class Outer {
+  class Inner
+}
+
+trait Locals {
+  def f0 = {
+    var x = 1 // warn
+    var y = 2 // no warn
+    y = 3
+    y + y
+  }
+  def f1 = {
+    val a = new Outer // no warn
+    val b = new Outer // warn
+    new a.Inner
+  }
+  def f2 = {
+    var x = 100 // warn about it being a var
+    x
+  }
+}
+
+object Types {
+  def l1() = {
+    object HiObject { def f = this } // warn
+    class Hi { // warn
+      def f1: Hi = new Hi
+      def f2(x: Hi) = x
+    }
+    class DingDongDoobie // warn
+    class Bippy // no warn
+    type Something = Bippy // no warn
+    type OtherThing = String // warn
+    (new Bippy): Something
+  }
+}
+
+// breakage: local val x$1 in method skolemize is never used
+case class SymbolKind(accurate: String, sanitized: String, abbreviation: String) {
+  def skolemize: SymbolKind = copy(accurate = s"$accurate skolem", abbreviation = s"$abbreviation#SKO")
+}
diff --git a/tests/warn/unused-params.scala b/tests/warn/unused-params.scala
new file mode 100644
index 000000000000..5ef339c942ac
--- /dev/null
+++ b/tests/warn/unused-params.scala
@@ -0,0 +1,159 @@
+//> using options -Wunused:params
+//
+
+import Answers._
+
+trait InterFace {
+  /** Call something. */
+  def call(a: Int, b: String, c: Double): Int
+}
+
+trait BadAPI extends InterFace {
+  def f(a: Int,
+        b: String,               // warn
+        c: Double): Int = {
+    println(c)
+    a
+  }
+  @deprecated("no warn in deprecated API", since="yesterday")
+  def g(a: Int,
+        b: String,               // no warn
+        c: Double): Int = {
+    println(c)
+    a
+  }
+  override def call(a: Int,
+                    b: String, // warn
+                    c: Double): Int = {
+    println(c)
+    a
+  }
+
+  def meth(x: Int) = x
+
+  override def equals(other: Any): Boolean = true  // no warn
+
+  def i(implicit s: String) = answer           // warn
+
+  /*
+  def future(x: Int): Int = {
+    val y = 42
+    val x = y               // maybe option to warn only if shadowed
+    x
+  }
+  */
+}
+
+// mustn't alter warnings in super
+trait PoorClient extends BadAPI {
+  override def meth(x: Int) = ???       // no warn
+  override def f(a: Int, b: String, c: Double): Int = a + b.toInt + c.toInt
+}
+
+class Unusing(u: Int) {       // warn
+  def f = ???
+}
+
+class Valuing(val u: Int)        // no warn
+
+class Revaluing(u: Int) { def f = u } // no warn
+
+case class CaseyKasem(k: Int)        // no warn
+
+case class CaseyAtTheBat(k: Int)(s: String)  // warn
+
+trait Ignorance {
+  def f(readResolve: Int) = answer           // warn
+}
+
+class Reusing(u: Int) extends Unusing(u)   // no warn
+
+class Main {
+  def main(args: Array[String]): Unit = println("hello, args")  // no warn
+}
+
+trait Unimplementation {
+  def f(u: Int): Int = ???        // no warn for param in unimplementation
+}
+
+trait DumbStuff {
+  def f(implicit dummy: DummyImplicit) = answer
+  def g(dummy: DummyImplicit) = answer // warn
+}
+trait Proofs {
+  def f[A, B](implicit ev: A =:= B) = answer
+  def g[A, B](implicit ev: A <:< B) = answer
+  def f2[A, B](ev: A =:= B) = answer // warn
+  def g2[A, B](ev: A <:< B) = answer // warn
+}
+
+trait Anonymous {
+  def f = (i: Int) => answer      // warn
+
+  def f1 = (_: Int) => answer     // no warn underscore parameter (a fresh name)
+
+  def f2: Int => Int = _ + 1  // no warn placeholder syntax (a fresh name and synthetic parameter)
+
+  def g = for (i <- List(1)) yield answer    // no warn patvar elaborated as map.(i => 42)
+}
+trait Context[A] { def m(a: A): A = a }
+trait Implicits {
+  def f[A](implicit ctx: Context[A]) = answer // warn
+  def g[A: Context] = answer // warn
+  def h[A](using Context[A]) = answer // warn
+}
+class Bound[A: Context] // warn
+object Answers {
+  def answer: Int = 42
+}
+
+trait BadMix { self: InterFace =>
+  def f(a: Int,
+        b: String,               // warn
+        c: Double): Int = {
+    println(c)
+    a
+  }
+  @deprecated("no warn in deprecated API", since="yesterday")
+  def g(a: Int,
+        b: String,               // no warn
+        c: Double): Int = {
+    println(c)
+    a
+  }
+  override def call(a: Int,
+                    XXXX: String,               // warn no longer excused because required by superclass
+                    c: Double): Int = {
+    println(c)
+    a
+  }
+
+  def meth(x: Int) = x
+
+  override def equals(other: Any): Boolean = true  // no warn
+
+  def i(implicit s: String) = answer           // warn
+}
+
+class Unequal {
+  override def equals(other: Any) = toString.nonEmpty   // warn
+}
+
+class Seriously {
+  def f(s: Serializable) = toString.nonEmpty  // warn explicit param of marker trait
+}
+
+class TryStart(start: String) {
+  def FINALLY(end: END.type) = start // no warn for DSL taking a singleton
+}
+
+object END
+
+object Optional:
+  extension (opt: Option.type) // no warn for extension of module
+    @annotation.experimental
+    inline def fromNullable[T](t: T | Null): Option[T] = Option(t).asInstanceOf[Option[T]]
+
+class Nested {
+  @annotation.unused private def actuallyNotUsed(fresh: Int, stale: Int) = fresh // no warn if owner is unused
+}
diff --git a/tests/warn/unused-privates.scala b/tests/warn/unused-privates.scala
new file mode 100644
index 000000000000..8864bc16de2b
--- /dev/null
+++ b/tests/warn/unused-privates.scala
@@ -0,0 +1,310 @@
+//
+//> using options -deprecation -Wunused:privates,locals
+//
+class Bippy(a: Int, b: Int) {
+  private def this(c: Int) = this(c, c)           // warn (DO warn, was NO)
+  private def bippy(x: Int): Int      = bippy(x)  // warn
+  private def boop(x: Int)            = x+a+b     // warn
+  final private val MILLIS1           = 2000      // warn, scala2: no warn, might have been inlined
+  final private val MILLIS2: Int      = 1000      // warn
+  final private val HI_COMPANION: Int = 500       // no warn, accessed from companion
+  def hi() = Bippy.HI_INSTANCE
+}
+object Bippy {
+  def hi(x: Bippy) = x.HI_COMPANION
+  private val HI_INSTANCE: Int = 500      // no warn, accessed from instance
+  private val HEY_INSTANCE: Int = 1000    // warn
+  private lazy val BOOL: Boolean = true   // warn
+}
+
+class A(val msg: String)
+class B1(msg: String) extends A(msg)
+class B2(msg0: String) extends A(msg0)
+class B3(msg0: String) extends A("msg")
+
+trait Accessors {
+  private var v1: Int = 0 // warn
+  private var v2: Int = 0 // warn, never set
+  private var v3: Int = 0 // warn, never got
+  private var v4: Int = 0 // no warn
+
+  private var v5 = 0 // warn, never set
+  private var v6 = 0 // warn, never got
+  private var v7 = 0 // no warn
+
+  def bippy(): Int = {
+    v3 = 3
+    v4 = 4
+    v6 = 6
+    v7 = 7
+    v2 + v4 + v5 + v7
+  }
+}
+
+class StableAccessors {
+  private var s1: Int = 0 // warn
+  private var s2: Int = 0 // warn, never set
+  private var s3: Int = 0 // warn, never got
+  private var s4: Int = 0 // no warn
+
+  private var s5 = 0 // warn, never set
+  private var s6 = 0 // warn, never got
+  private var s7 = 0 // no warn
+
+  def bippy(): Int = {
+    s3 = 3
+    s4 = 4
+    s6 = 6
+    s7 = 7
+    s2 + s4 + s5 + s7
+  }
+}
+
+trait DefaultArgs {
+  // DO warn about default getters for x2 and x3
+  private def bippy(x1: Int, x2: Int = 10, x3: Int = 15): Int = x1 + x2 + x3 // warn // warn
+
+  def boppy() = bippy(5, 100, 200)
+}
+
+/* scala/bug#7707 Both usages warn default arg because using PrivateRyan.apply, not new.
+case class PrivateRyan private (ryan: Int = 42) { def f = PrivateRyan() }
+object PrivateRyan { def f = PrivateRyan() }
+*/
+
+class Outer {
+  class Inner
+}
+
+trait Locals {
+  def f0 = {
+    var x = 1 // warn
+    var y = 2
+    y = 3
+    y + y
+  }
+  def f1 = {
+    val a = new Outer // no warn
+    val b = new Outer // warn
+    new a.Inner
+  }
+  def f2 = {
+    var x = 100 // warn about it being a var
+    x
+  }
+}
+
+object Types {
+  private object Dongo { def f = this } // warn
+  private class Bar1 // warn
+  private class Bar2 // no warn
+  private type Alias1 = String // warn
+  private type Alias2 = String // no warn
+  def bippo = (new Bar2).toString
+
+  def f(x: Alias2) = x.length
+
+  def l1() = {
+    object HiObject { def f = this } // warn
+    class Hi { // warn
+      def f1: Hi = new Hi
+      def f2(x: Hi) = x
+    }
+    class DingDongDoobie // warn
+    class Bippy // no warn
+    type Something = Bippy // no warn
+    type OtherThing = String // warn
+    (new Bippy): Something
+  }
+}
+
+trait Underwarn {
+  def f(): Seq[Int]
+
+  def g() = {
+    val Seq(_, _) = f()  // no warn
+    true
+  }
+}
+
+class OtherNames {
+  private def x_=(i: Int): Unit = () // warn
+  private def x: Int = 42 // warn
+  private def y_=(i: Int): Unit = () // warn
+  private def y: Int = 42
+
+  def f = y
+}
+
+case class C(a: Int, b: String, c: Option[String])
+case class D(a: Int)
+
+// patvars which used to warn as vals in older scala 2
+trait Boundings {
+
+  def c = C(42, "hello", Some("world"))
+  def d = D(42)
+
+  def f() = {
+    val C(x, y, Some(z)) = c: @unchecked              // no warn
+    17
+  }
+  def g() = {
+    val C(x @ _, y @ _, Some(z @ _)) = c: @unchecked  // no warn
+    17
+  }
+  def h() = {
+    val C(x @ _, y @ _, z @ Some(_)) = c: @unchecked  // no warn for z?
+    17
+  }
+
+  def v() = {
+    val D(x) = d                          // no warn
+    17
+  }
+  def w() = {
+    val D(x @ _) = d                      // no warn
+    17
+  }
+
+}
+
+trait Forever {
+  def f = {
+    val t = Option((17, 42))
+    for {
+      ns <- t
+      (i, j) = ns                        // no warn
+    } yield (i + j)
+  }
+  def g = {
+    val t = Option((17, 42))
+    for {
+      ns <- t
+      (i, j) = ns                        // no warn
+    } yield 42                           // val emitted only if needed, hence nothing unused
+  }
+}
+
+trait Ignorance {
+  private val readResolve = 42      // warn wrong signatured for special members
+}
+
+trait CaseyKasem {
+  def f = 42 match {
+    case x if x < 25 => "no warn"
+    case y if toString.nonEmpty => "no warn" + y
+    case z => "warn"
+  }
+}
+trait CaseyAtTheBat {
+  def f = Option(42) match {
+    case Some(x) if x < 25 => "no warn"
+    case Some(y @ _) if toString.nonEmpty => "no warn"
+    case Some(z) => "warn"
+    case None => "no warn"
+  }
+}
+
+class `not even using companion privates`
+
+object `not even using companion privates` {
+  private implicit class `for your eyes only`(i: Int) {  // warn
+    def f = i
+  }
+}
+
+class `no warn in patmat anonfun isDefinedAt` {
+  def f(pf: PartialFunction[String, Int]) = pf("42")
+  def g = f {
+    case s => s.length        // no warn (used to warn case s => true in isDefinedAt)
+  }
+}
+
+// this is the ordinary case, as AnyRef is an alias of Object
+class `nonprivate alias is enclosing` {
+  class C
+  type C2 = C
+  private class D extends C2   // warn
+}
+
+object `classof something` {
+  private class intrinsically
+  def f = classOf[intrinsically].toString()
+}
+
+trait `scala 2 short comings` {
+  def f: Int = {
+    val x = 42 // warn
+    17
+  }
+}
+
+class `issue 12600 ignore abstract types` {
+  type Abs
+}
+
+class `t12992 enclosing def is unused` {
+  private val n = 42
+  @annotation.unused def f() = n + 2 // unused code uses n
+}
+
+class `recursive reference is not a usage` {
+  private def f(i: Int): Int = // warn
+    if (i <= 0) i
+    else f(i-1)
+  private class P { // warn
+    def f() = new P()
+  }
+}
+
+class `absolve serial framework` extends Serializable:
+  import java.io.{IOException, ObjectInputStream, ObjectOutputStream, ObjectStreamException}
+  @throws(classOf[IOException])
+  private def writeObject(stream: ObjectOutputStream): Unit = ()
+  @throws(classOf[ObjectStreamException])
+  private def writeReplace(): Object = ???
+  @throws(classOf[ClassNotFoundException])
+  @throws(classOf[IOException])
+  private def readObject(stream: ObjectInputStream): Unit = ()
+  @throws(classOf[ObjectStreamException])
+  private def readObjectNoData(): Unit = ()
+  @throws(classOf[ObjectStreamException])
+  private def readResolve(): Object = ???
+
+class `absolve ONLY serial framework`:
+  import java.io.{IOException, ObjectInputStream, ObjectOutputStream, ObjectStreamException}
+  @throws(classOf[IOException])
+  private def writeObject(stream: ObjectOutputStream): Unit = () // warn
+  @throws(classOf[ObjectStreamException])
+  private def writeReplace(): Object = new Object // warn
+  @throws(classOf[ClassNotFoundException])
+  @throws(classOf[IOException])
+  private def readObject(stream: ObjectInputStream): Unit = () // warn
+  @throws(classOf[ObjectStreamException])
+  private def readObjectNoData(): Unit = () // warn
+  @throws(classOf[ObjectStreamException])
+  private def readResolve(): Object = new Object // warn
+
+@throws(classOf[java.io.ObjectStreamException])
+private def readResolve(): Object = ??? // TODO warn
+private def print() = println() // TODO warn
+private val printed = false // TODO warn
+
+package locked:
+  private[locked] def locker(): Unit = () // TODO warn as we cannot distinguish unqualified private at top level
+  package basement:
+    private[locked] def shackle(): Unit = () // no warn as it is not top level at boundary
+
+object `i19998 refinement`:
+  trait Foo {
+    type X[a]
+  }
+  trait Bar[X[_]] {
+    private final type SelfX[a] = X[a] // was false positive
+    val foo: Foo { type X[a] = SelfX[a] }
+  }
+
+object `patvar is assignable`:
+  private var (i, j) = (42, 27) // no warn patvars under -Wunused:privates
+  println((i, j))