Skip to content

Commit 612769d

Browse files
som-snytttgodzik
authored andcommitted
Report unused masking import selectors
[Cherry-picked 8b73a79]
1 parent 74d6fd3 commit 612769d

File tree

4 files changed

+36
-14
lines changed

4 files changed

+36
-14
lines changed

compiler/src/dotty/tools/dotc/ast/untpd.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
138138
case Ident(rename: TermName) => rename
139139
case _ => name
140140

141+
/** It's a masking import if `!isWildcard`. */
141142
def isUnimport = rename == nme.WILDCARD
142143
}
143144

compiler/src/dotty/tools/dotc/transform/CheckUnused.scala

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import dotty.tools.dotc.util.chaining.*
2525

2626
import java.util.IdentityHashMap
2727

28+
import scala.annotation.*
2829
import scala.collection.mutable, mutable.{ArrayBuilder, ListBuffer, Stack}
2930

3031
import CheckUnused.*
@@ -277,6 +278,8 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
277278
alt.symbol == sym
278279
|| nm.isTypeName && alt.symbol.isAliasType && alt.info.dealias.typeSymbol == sym
279280
sameSym && alt.symbol.isAccessibleFrom(qtpe)
281+
def hasAltMemberNamed(nm: Name) = qtpe.member(nm).hasAltWith(_.symbol.isAccessibleFrom(qtpe))
282+
280283
def loop(sels: List[ImportSelector]): ImportSelector | Null = sels match
281284
case sel :: sels =>
282285
val matches =
@@ -293,9 +296,17 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
293296
else
294297
!sym.is(Given) // Normal wildcard, check that the symbol is not a given (but can be implicit)
295298
}
299+
else if sel.isUnimport then
300+
val masksMatchingMember =
301+
name != nme.NO_NAME
302+
&& sels.exists(x => x.isWildcard && !x.isGiven)
303+
&& !name.exists(_.toTermName != sel.name) // import a.b as _, b must match name
304+
&& (hasAltMemberNamed(sel.name) || hasAltMemberNamed(sel.name.toTypeName))
305+
if masksMatchingMember then
306+
refInfos.sels.put(sel, ()) // imprecise due to precedence but errs on the side of false negative
307+
false
296308
else
297-
// if there is an explicit name, it must match
298-
!name.exists(_.toTermName != sel.rename)
309+
!name.exists(_.toTermName != sel.rename) // if there is an explicit name, it must match
299310
&& (prefix.eq(NoPrefix) || qtpe =:= prefix)
300311
&& (hasAltMember(sel.name) || hasAltMember(sel.name.toTypeName))
301312
if matches then sel else loop(sels)
@@ -626,11 +637,11 @@ object CheckUnused:
626637
warnAt(pos)(UnusedSymbol.unsetPrivates)
627638

628639
def checkImports() =
629-
// TODO check for unused masking import
630640
import scala.jdk.CollectionConverters.given
631641
import Rewrites.ActionPatch
632642
type ImpSel = (Import, ImportSelector)
633-
// true if used or might be used, to imply don't warn about it
643+
def isUsed(sel: ImportSelector): Boolean = infos.sels.containsKey(sel)
644+
@unused // avoid merge conflict
634645
def isUsable(imp: Import, sel: ImportSelector): Boolean =
635646
sel.isImportExclusion || infos.sels.containsKey(sel)
636647
def warnImport(warnable: ImpSel, actions: List[CodeAction] = Nil): Unit =
@@ -641,7 +652,7 @@ object CheckUnused:
641652
warnAt(sel.srcPos)(msg, origin)
642653

643654
if !actionable then
644-
for imp <- infos.imps.keySet.nn.asScala; sel <- imp.selectors if !isUsable(imp, sel) do
655+
for imp <- infos.imps.keySet.nn.asScala; sel <- imp.selectors if !isUsed(sel) do
645656
warnImport(imp -> sel)
646657
else
647658
// If the rest of the line is blank, include it in the final edit position. (Delete trailing whitespace.)
@@ -696,7 +707,7 @@ object CheckUnused:
696707
while index < sortedImps.length do
697708
val nextImport = sortedImps.indexSatisfying(from = index + 1)(_.isPrimaryClause) // next import statement
698709
if sortedImps.indexSatisfying(from = index, until = nextImport): imp =>
699-
imp.selectors.exists(!isUsable(imp, _)) // check if any selector in statement was unused
710+
imp.selectors.exists(!isUsed(_)) // check if any selector in statement was unused
700711
< nextImport then
701712
// if no usable selectors in the import statement, delete it entirely.
702713
// if there is exactly one usable selector, then replace with just that selector (i.e., format it).
@@ -705,7 +716,7 @@ object CheckUnused:
705716
// Reminder that first clause span includes the keyword, so delete point-to-start instead.
706717
val existing = sortedImps.slice(index, nextImport)
707718
val (keeping, deleting) = existing.iterator.flatMap(imp => imp.selectors.map(imp -> _)).toList
708-
.partition(isUsable(_, _))
719+
.partition((imp, sel) => isUsed(sel))
709720
if keeping.isEmpty then
710721
val editPos = existing.head.srcPos.sourcePos.withSpan:
711722
Span(start = existing.head.srcPos.span.start, end = existing.last.srcPos.span.end)
@@ -930,12 +941,11 @@ object CheckUnused:
930941
def boundTpe: Type = sel.bound match
931942
case untpd.TypedSplice(tree) => tree.tpe
932943
case _ => NoType
933-
/** This is used to ignore exclusion imports of the form import `qual.member as _`
934-
* because `sel.isUnimport` is too broad for old style `import concurrent._`.
944+
/** Is a "masking" import of the form import `qual.member as _`.
945+
* Both conditions must be checked.
935946
*/
936-
def isImportExclusion: Boolean = sel.renamed match
937-
case untpd.Ident(nme.WILDCARD) => true
938-
case _ => false
947+
@unused // matchingSelector checks isWildcard first
948+
def isImportExclusion: Boolean = sel.isUnimport && !sel.isWildcard
939949

940950
extension (imp: Import)(using Context)
941951
/** Is it the first import clause in a statement? `a.x` in `import a.x, b.{y, z}` */

tests/warn/i15503a.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,12 @@ object InnerMostCheck:
8585
val a = Set(1)
8686

8787
object IgnoreExclusion:
88-
import collection.mutable.{Set => _} // OK
89-
import collection.mutable.{Map => _} // OK
88+
import collection.mutable.{Map => _, Set => _, *} // OK??
9089
import collection.mutable.{ListBuffer} // warn
9190
def check =
9291
val a = Set(1)
9392
val b = Map(1 -> 2)
93+
def c = Seq(42)
9494
/**
9595
* Some given values for the test
9696
*/

tests/warn/i23758.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//> using options -Wunused:imports
2+
3+
import scala.util.Try as _ // warn
4+
5+
class Promise(greeting: String):
6+
override def toString = greeting
7+
8+
@main def test = println:
9+
import scala.concurrent.{Promise as _, *}, ExecutionContext.Implicits.given
10+
val promise = new Promise("world")
11+
Future(s"hello, $promise")

0 commit comments

Comments
 (0)