Skip to content

Commit 90e769a

Browse files
committed
Report unused masking import selectors
1 parent b3e9ff9 commit 90e769a

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.*
@@ -309,6 +310,8 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
309310
alt.symbol == sym
310311
|| nm.isTypeName && alt.symbol.isAliasType && alt.info.dealias.typeSymbol == sym
311312
sameSym && alt.symbol.isAccessibleFrom(qtpe)
313+
def hasAltMemberNamed(nm: Name) = qtpe.member(nm).hasAltWith(_.symbol.isAccessibleFrom(qtpe))
314+
312315
def loop(sels: List[ImportSelector]): ImportSelector | Null = sels match
313316
case sel :: sels =>
314317
val matches =
@@ -325,9 +328,17 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
325328
else
326329
!sym.is(Given) // Normal wildcard, check that the symbol is not a given (but can be implicit)
327330
}
331+
else if sel.isUnimport then
332+
val masksMatchingMember =
333+
name != nme.NO_NAME
334+
&& sels.exists(x => x.isWildcard && !x.isGiven)
335+
&& !name.exists(_.toTermName != sel.name) // import a.b as _, b must match name
336+
&& (hasAltMemberNamed(sel.name) || hasAltMemberNamed(sel.name.toTypeName))
337+
if masksMatchingMember then
338+
refInfos.sels.put(sel, ()) // imprecise due to precedence but errs on the side of false negative
339+
false
328340
else
329-
// if there is an explicit name, it must match
330-
!name.exists(_.toTermName != sel.rename)
341+
!name.exists(_.toTermName != sel.rename) // if there is an explicit name, it must match
331342
&& (prefix.eq(NoPrefix) || qtpe =:= prefix)
332343
&& (hasAltMember(sel.name) || hasAltMember(sel.name.toTypeName))
333344
if matches then sel else loop(sels)
@@ -658,11 +669,11 @@ object CheckUnused:
658669
warnAt(pos)(UnusedSymbol.unsetPrivates)
659670

660671
def checkImports() =
661-
// TODO check for unused masking import
662672
import scala.jdk.CollectionConverters.given
663673
import Rewrites.ActionPatch
664674
type ImpSel = (Import, ImportSelector)
665-
// true if used or might be used, to imply don't warn about it
675+
def isUsed(sel: ImportSelector): Boolean = infos.sels.containsKey(sel)
676+
@unused // avoid merge conflict
666677
def isUsable(imp: Import, sel: ImportSelector): Boolean =
667678
sel.isImportExclusion || infos.sels.containsKey(sel)
668679
def warnImport(warnable: ImpSel, actions: List[CodeAction] = Nil): Unit =
@@ -673,7 +684,7 @@ object CheckUnused:
673684
warnAt(sel.srcPos)(msg, origin)
674685

675686
if !actionable then
676-
for imp <- infos.imps.keySet.nn.asScala; sel <- imp.selectors if !isUsable(imp, sel) do
687+
for imp <- infos.imps.keySet.nn.asScala; sel <- imp.selectors if !isUsed(sel) do
677688
warnImport(imp -> sel)
678689
else
679690
// If the rest of the line is blank, include it in the final edit position. (Delete trailing whitespace.)
@@ -728,7 +739,7 @@ object CheckUnused:
728739
while index < sortedImps.length do
729740
val nextImport = sortedImps.indexSatisfying(from = index + 1)(_.isPrimaryClause) // next import statement
730741
if sortedImps.indexSatisfying(from = index, until = nextImport): imp =>
731-
imp.selectors.exists(!isUsable(imp, _)) // check if any selector in statement was unused
742+
imp.selectors.exists(!isUsed(_)) // check if any selector in statement was unused
732743
< nextImport then
733744
// if no usable selectors in the import statement, delete it entirely.
734745
// if there is exactly one usable selector, then replace with just that selector (i.e., format it).
@@ -737,7 +748,7 @@ object CheckUnused:
737748
// Reminder that first clause span includes the keyword, so delete point-to-start instead.
738749
val existing = sortedImps.slice(index, nextImport)
739750
val (keeping, deleting) = existing.iterator.flatMap(imp => imp.selectors.map(imp -> _)).toList
740-
.partition(isUsable(_, _))
751+
.partition((imp, sel) => isUsed(sel))
741752
if keeping.isEmpty then
742753
val editPos = existing.head.srcPos.sourcePos.withSpan:
743754
Span(start = existing.head.srcPos.span.start, end = existing.last.srcPos.span.end)
@@ -962,12 +973,11 @@ object CheckUnused:
962973
def boundTpe: Type = sel.bound match
963974
case untpd.TypedSplice(tree) => tree.tpe
964975
case _ => NoType
965-
/** This is used to ignore exclusion imports of the form import `qual.member as _`
966-
* because `sel.isUnimport` is too broad for old style `import concurrent._`.
976+
/** Is a "masking" import of the form import `qual.member as _`.
977+
* Both conditions must be checked.
967978
*/
968-
def isImportExclusion: Boolean = sel.renamed match
969-
case untpd.Ident(nme.WILDCARD) => true
970-
case _ => false
979+
@unused // matchingSelector checks isWildcard first
980+
def isImportExclusion: Boolean = sel.isUnimport && !sel.isWildcard
971981

972982
extension (imp: Import)(using Context)
973983
/** 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)