From c61872cc8ca1f9dba9324416bd48c65d073f5b06 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Wed, 5 Mar 2025 15:34:31 -0800 Subject: [PATCH 1/2] Warn unused member of anonymous class --- .../dotty/tools/dotc/transform/CheckUnused.scala | 7 +++++-- tests/semanticdb/metac.expect | 3 ++- tests/warn/i22681.scala | 13 +++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 tests/warn/i22681.scala diff --git a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala index 6b2a987dca8c..b584b62596b4 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala @@ -520,7 +520,7 @@ object CheckUnused: def checkPrivate(sym: Symbol, pos: SrcPos) = if ctx.settings.WunusedHas.privates && !sym.isPrimaryConstructor - && sym.is(Private, butNot = SelfName | Synthetic | CaseAccessor) + && !sym.isOneOf(SelfName | Synthetic | CaseAccessor) && !sym.name.is(BodyRetainerName) && !sym.isSerializationSupport && !(sym.is(Mutable) && sym.isSetter && sym.owner.is(Trait)) // tracks sym.underlyingSymbol sibling getter @@ -764,7 +764,7 @@ object CheckUnused: 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 + else if sym.isEffectivelyPrivate then checkPrivate(sym, pos) else if sym.is(Param, butNot = Given | Implicit) then checkParam(sym, pos) @@ -885,6 +885,9 @@ object CheckUnused: 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 isEffectivelyPrivate(using Context): Boolean = + sym.is(Private, butNot = ParamAccessor) + || sym.owner.isAnonymousClass && !sym.nextOverriddenSymbol.exists extension (sel: ImportSelector) def boundTpe: Type = sel.bound match diff --git a/tests/semanticdb/metac.expect b/tests/semanticdb/metac.expect index f674c6fb4159..fa32cd68b652 100644 --- a/tests/semanticdb/metac.expect +++ b/tests/semanticdb/metac.expect @@ -3494,7 +3494,7 @@ Text => empty Language => Scala Symbols => 12 entries Occurrences => 33 entries -Diagnostics => 1 entries +Diagnostics => 2 entries Synthetics => 4 entries Symbols: @@ -3550,6 +3550,7 @@ Diagnostics: [14:20..14:23): [warning] Alphanumeric method foo is not declared infix; it should not be used as infix operator. Instead, use method syntax .foo(...) or backticked identifier `foo`. The latter can be rewritten automatically under -rewrite -source 3.4-migration. +[19:8..19:17): [warning] unused private member Synthetics: [12:2..12:6):user => reflectiveSelectable(*) diff --git a/tests/warn/i22681.scala b/tests/warn/i22681.scala new file mode 100644 index 000000000000..38875dad7994 --- /dev/null +++ b/tests/warn/i22681.scala @@ -0,0 +1,13 @@ + +//> using options -Wunused:all + +trait T: + def t: Int + +class C: + def f: Runnable { def u: Int } = new Runnable with T: + private def v = 42 // avoid g judged too trivial to warn + def run() = () + def g = v // warn effectively private member is unused + def t = v // nowarn + def u = v // warn despite structural type (TODO work around the limitation, at least for this example) From 725bd8cf1c2bae8d4be00377ee6cc9a75666a7b2 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Sat, 8 Mar 2025 01:34:28 -0800 Subject: [PATCH 2/2] First cut at nowarn refinement members --- .../tools/dotc/transform/CheckUnused.scala | 22 +++++++++++++++++-- tests/semanticdb/metac.expect | 3 +-- tests/warn/i22681.scala | 6 ++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala index b584b62596b4..2611652a110b 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala @@ -150,6 +150,9 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha override def prepareForValDef(tree: ValDef)(using Context): Context = if !tree.symbol.is(Deferred) && tree.rhs.symbol != defn.Predef_undefined then refInfos.register(tree) + tree.tpt match + case RefinedTypeTree(_, refinements) => relax(tree.rhs, refinements) + case _ => ctx override def transformValDef(tree: ValDef)(using Context): tree.type = traverseAnnotations(tree.symbol) @@ -170,11 +173,15 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha 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 !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) + tree.tpt match + case RefinedTypeTree(_, refinements) => relax(tree.rhs, refinements) + case _ => ctx override def transformDefDef(tree: DefDef)(using Context): tree.type = traverseAnnotations(tree.symbol) @@ -456,7 +463,7 @@ object CheckUnused: if !tree.name.isInstanceOf[DerivedName] then pats.addOne((tree.symbol, tree.namePos)) case tree: NamedDefTree => - if (tree.symbol ne NoSymbol) && !tree.name.isWildcard then + if (tree.symbol ne NoSymbol) && !tree.name.isWildcard && !tree.hasAttachment(NoWarn) then defs.addOne((tree.symbol, tree.namePos)) case _ => if tree.symbol ne NoSymbol then @@ -851,6 +858,17 @@ object CheckUnused: args.foreach(traverse) case tree => traverseChildren(tree) + // NoWarn members in tree that correspond to refinements; currently uses only names. + def relax(tree: Tree, refinements: List[Tree])(using Context): Unit = + val names = refinements.collect { case named: NamedDefTree => named.name }.toSet + val relaxer = new TreeTraverser: + def traverse(tree: Tree)(using Context) = + tree match + case tree: NamedDefTree if names(tree.name) => tree.withAttachment(NoWarn, ()) + case _ => + traverseChildren(tree) + relaxer.traverse(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) diff --git a/tests/semanticdb/metac.expect b/tests/semanticdb/metac.expect index fa32cd68b652..f674c6fb4159 100644 --- a/tests/semanticdb/metac.expect +++ b/tests/semanticdb/metac.expect @@ -3494,7 +3494,7 @@ Text => empty Language => Scala Symbols => 12 entries Occurrences => 33 entries -Diagnostics => 2 entries +Diagnostics => 1 entries Synthetics => 4 entries Symbols: @@ -3550,7 +3550,6 @@ Diagnostics: [14:20..14:23): [warning] Alphanumeric method foo is not declared infix; it should not be used as infix operator. Instead, use method syntax .foo(...) or backticked identifier `foo`. The latter can be rewritten automatically under -rewrite -source 3.4-migration. -[19:8..19:17): [warning] unused private member Synthetics: [12:2..12:6):user => reflectiveSelectable(*) diff --git a/tests/warn/i22681.scala b/tests/warn/i22681.scala index 38875dad7994..e15d186479c3 100644 --- a/tests/warn/i22681.scala +++ b/tests/warn/i22681.scala @@ -10,4 +10,8 @@ class C: def run() = () def g = v // warn effectively private member is unused def t = v // nowarn - def u = v // warn despite structural type (TODO work around the limitation, at least for this example) + def u = v // nowarn because leaked by refinement + val v: Runnable { def u: Int } = new Runnable: + private def v = 42 // avoid g judged too trivial to warn + def run() = () + def u = v // nowarn because leaked by refinement