Skip to content

Commit 5f88203

Browse files
authored
Merge pull request #606 from scala/backport-lts-3.3-23757
Backport "Warn unused masking imports, remove obsolete unused options" to 3.3 LTS
2 parents 92d1387 + f4150e4 commit 5f88203

File tree

9 files changed

+130
-77
lines changed

9 files changed

+130
-77
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/config/ScalaSettings.scala

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,6 @@ private sealed trait WarningSettings:
193193
ChoiceWithHelp("patvars","Warn if a variable bound in a pattern is unused"),
194194
//ChoiceWithHelp("inlined", "Apply -Wunused to inlined expansions"), // TODO
195195
ChoiceWithHelp("linted", "Enable -Wunused:imports,privates,locals,implicits"),
196-
ChoiceWithHelp(
197-
name = "strict-no-implicit-warn",
198-
description = """Same as -Wunused:imports, only for imports of explicit named members.
199-
|NOTE : This overrides -Wunused:imports and NOT set by -Wunused:all""".stripMargin
200-
),
201-
ChoiceWithHelp("unsafe-warn-patvars", "Deprecated alias for `patvars`"),
202196
),
203197
default = Nil
204198
)

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

Lines changed: 69 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import dotty.tools.dotc.config.ScalaSettings
77
import dotty.tools.dotc.core.Contexts.*
88
import dotty.tools.dotc.core.Flags.*
99
import dotty.tools.dotc.core.Names.{Name, SimpleName, DerivedName, TermName, termName}
10-
import dotty.tools.dotc.core.NameOps.{isAnonymousFunctionName, isReplWrapperName, isContextFunction, setterName}
1110
import dotty.tools.dotc.core.NameKinds.{
1211
BodyRetainerName, ContextBoundParamName, ContextFunctionParamName, DefaultGetterName, WildcardParamName}
12+
import dotty.tools.dotc.core.NameOps.{isAnonymousFunctionName, isReplWrapperName, isContextFunction, setterName}
13+
import dotty.tools.dotc.core.Scopes.newScope
1314
import dotty.tools.dotc.core.StdNames.nme
1415
import dotty.tools.dotc.core.Symbols.{ClassSymbol, NoSymbol, Symbol, defn, isDeprecated, requiredClass, requiredModule}
1516
import dotty.tools.dotc.core.Types.*
@@ -19,6 +20,7 @@ import dotty.tools.dotc.rewrites.Rewrites
1920
import dotty.tools.dotc.transform.MegaPhase.MiniPhase
2021
import dotty.tools.dotc.typer.{ImportInfo, Typer}
2122
import dotty.tools.dotc.typer.Deriving.OriginalTypeClass
23+
import dotty.tools.dotc.typer.Implicits.{ContextualImplicits, RenamedImplicitRef}
2224
import dotty.tools.dotc.util.{Property, Spans, SrcPos}, Spans.Span
2325
import dotty.tools.dotc.util.Chars.{isLineBreakChar, isWhitespace}
2426
import dotty.tools.dotc.util.chaining.*
@@ -115,6 +117,14 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
115117
args.foreach(_.withAttachment(ForArtifact, ()))
116118
case _ =>
117119
ctx
120+
override def transformApply(tree: Apply)(using Context): tree.type =
121+
// check for multiversal equals
122+
tree match
123+
case Apply(Select(left, nme.Equals | nme.NotEquals), right :: Nil) =>
124+
val caneq = defn.CanEqualClass.typeRef.appliedTo(left.tpe.widen :: right.tpe.widen :: Nil)
125+
resolveScoped(caneq)
126+
case _ =>
127+
tree
118128

119129
override def prepareForAssign(tree: Assign)(using Context): Context =
120130
tree.lhs.putAttachment(AssignmentTarget, ()) // don't take LHS reference as a read
@@ -195,6 +205,16 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
195205
refInfos.register(tree)
196206
tree
197207

208+
override def prepareForStats(trees: List[Tree])(using Context): Context =
209+
// gather local implicits while ye may
210+
if !ctx.owner.isClass then
211+
if trees.exists(t => t.isDef && t.symbol.is(Given) && t.symbol.isLocalToBlock) then
212+
val scope = newScope.openForMutations
213+
for tree <- trees if tree.isDef && tree.symbol.is(Given) do
214+
scope.enter(tree.symbol.name, tree.symbol)
215+
return ctx.fresh.setScope(scope)
216+
ctx
217+
198218
override def transformOther(tree: Tree)(using Context): tree.type =
199219
tree match
200220
case imp: Import =>
@@ -277,6 +297,8 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
277297
alt.symbol == sym
278298
|| nm.isTypeName && alt.symbol.isAliasType && alt.info.dealias.typeSymbol == sym
279299
sameSym && alt.symbol.isAccessibleFrom(qtpe)
300+
def hasAltMemberNamed(nm: Name) = qtpe.member(nm).hasAltWith(_.symbol.isAccessibleFrom(qtpe))
301+
280302
def loop(sels: List[ImportSelector]): ImportSelector | Null = sels match
281303
case sel :: sels =>
282304
val matches =
@@ -293,9 +315,17 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
293315
else
294316
!sym.is(Given) // Normal wildcard, check that the symbol is not a given (but can be implicit)
295317
}
318+
else if sel.isUnimport then
319+
val masksMatchingMember =
320+
name != nme.NO_NAME
321+
&& sels.exists(x => x.isWildcard && !x.isGiven)
322+
&& !name.exists(_.toTermName != sel.name) // import a.b as _, b must match name
323+
&& (hasAltMemberNamed(sel.name) || hasAltMemberNamed(sel.name.toTypeName))
324+
if masksMatchingMember then
325+
refInfos.sels.put(sel, ()) // imprecise due to precedence but errs on the side of false negative
326+
false
296327
else
297-
// if there is an explicit name, it must match
298-
!name.exists(_.toTermName != sel.rename)
328+
!name.exists(_.toTermName != sel.rename) // if there is an explicit name, it must match
299329
&& (prefix.eq(NoPrefix) || qtpe =:= prefix)
300330
&& (hasAltMember(sel.name) || hasAltMember(sel.name.toTypeName))
301331
if matches then sel else loop(sels)
@@ -363,6 +393,38 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
363393
if candidate != NoContext && candidate.isImportContext && importer != null then
364394
refInfos.sels.put(importer, ())
365395
end resolveUsage
396+
397+
/** Simulate implicit search for contextual implicits in lexical scope and mark any definitions or imports as used.
398+
* Avoid cached ctx.implicits because it needs the precise import context that introduces the given.
399+
*/
400+
def resolveScoped(tp: Type)(using Context): Unit =
401+
var done = false
402+
val ctxs = ctx.outersIterator
403+
while !done && ctxs.hasNext do
404+
val cur = ctxs.next()
405+
val implicitRefs: List[ImplicitRef] =
406+
if (cur.isClassDefContext) cur.owner.thisType.implicitMembers
407+
else if (cur.isImportContext) cur.importInfo.nn.importedImplicits
408+
else if (cur.isNonEmptyScopeContext) cur.scope.implicitDecls
409+
else Nil
410+
implicitRefs.find(ref => ref.underlyingRef.widen <:< tp) match
411+
case Some(found: TermRef) =>
412+
refInfos.addRef(found.denot.symbol)
413+
if cur.isImportContext then
414+
cur.importInfo.nn.selectors.find(sel => sel.isGiven || sel.rename == found.name) match
415+
case Some(sel) =>
416+
refInfos.sels.put(sel, ())
417+
case _ =>
418+
return
419+
case Some(found: RenamedImplicitRef) if cur.isImportContext =>
420+
refInfos.addRef(found.underlyingRef.denot.symbol)
421+
cur.importInfo.nn.selectors.find(sel => sel.rename == found.implicitName) match
422+
case Some(sel) =>
423+
refInfos.sels.put(sel, ())
424+
case _ =>
425+
return
426+
case _ =>
427+
end resolveScoped
366428
end CheckUnused
367429

368430
object CheckUnused:
@@ -568,7 +630,6 @@ object CheckUnused:
568630
|| m.is(Synthetic)
569631
|| m.hasAnnotation(dd.UnusedAnnot) // param of unused method
570632
|| sym.owner.name.isContextFunction // a ubiquitous parameter
571-
|| sym.isCanEqual
572633
|| sym.info.dealias.typeSymbol.match // more ubiquity
573634
case dd.DummyImplicitClass | dd.SubTypeClass | dd.SameTypeClass => true
574635
case tps =>
@@ -600,7 +661,6 @@ object CheckUnused:
600661
def checkLocal(sym: Symbol, pos: SrcPos) =
601662
if ctx.settings.WunusedHas.locals
602663
&& !sym.is(InlineProxy)
603-
&& !sym.isCanEqual
604664
then
605665
if sym.is(Mutable) && infos.asss(sym) then
606666
warnAt(pos)(UnusedSymbol.localVars)
@@ -628,12 +688,10 @@ object CheckUnused:
628688
warnAt(pos)(UnusedSymbol.unsetPrivates)
629689

630690
def checkImports() =
631-
// TODO check for unused masking import
632691
import scala.jdk.CollectionConverters.given
633692
import Rewrites.ActionPatch
634693
type ImpSel = (Import, ImportSelector)
635-
def isUsable(imp: Import, sel: ImportSelector): Boolean =
636-
sel.isImportExclusion || infos.sels.containsKey(sel) || imp.isLoose(sel)
694+
def isUsed(sel: ImportSelector): Boolean = infos.sels.containsKey(sel)
637695
def warnImport(warnable: ImpSel, actions: List[CodeAction] = Nil): Unit =
638696
val (imp, sel) = warnable
639697
val msg = UnusedSymbol.imports(actions)
@@ -642,7 +700,7 @@ object CheckUnused:
642700
warnAt(sel.srcPos)(msg, origin)
643701

644702
if !actionable then
645-
for imp <- infos.imps.keySet.nn.asScala; sel <- imp.selectors if !isUsable(imp, sel) do
703+
for imp <- infos.imps.keySet.nn.asScala; sel <- imp.selectors if !isUsed(sel) do
646704
warnImport(imp -> sel)
647705
else
648706
// If the rest of the line is blank, include it in the final edit position. (Delete trailing whitespace.)
@@ -697,7 +755,7 @@ object CheckUnused:
697755
while index < sortedImps.length do
698756
val nextImport = sortedImps.indexSatisfying(from = index + 1)(_.isPrimaryClause) // next import statement
699757
if sortedImps.indexSatisfying(from = index, until = nextImport): imp =>
700-
imp.selectors.exists(!isUsable(imp, _)) // check if any selector in statement was unused
758+
imp.selectors.exists(!isUsed(_)) // check if any selector in statement was unused
701759
< nextImport then
702760
// if no usable selectors in the import statement, delete it entirely.
703761
// if there is exactly one usable selector, then replace with just that selector (i.e., format it).
@@ -706,7 +764,7 @@ object CheckUnused:
706764
// Reminder that first clause span includes the keyword, so delete point-to-start instead.
707765
val existing = sortedImps.slice(index, nextImport)
708766
val (keeping, deleting) = existing.iterator.flatMap(imp => imp.selectors.map(imp -> _)).toList
709-
.partition(isUsable(_, _))
767+
.partition((imp, sel) => isUsed(sel))
710768
if keeping.isEmpty then
711769
val editPos = existing.head.srcPos.sourcePos.withSpan:
712770
Span(start = existing.head.srcPos.span.start, end = existing.last.srcPos.span.end)
@@ -908,8 +966,6 @@ object CheckUnused:
908966
def isSerializationSupport: Boolean =
909967
sym.is(Method) && serializationNames(sym.name.toTermName) && sym.owner.isClass
910968
&& sym.owner.derivesFrom(defn.JavaSerializableClass)
911-
def isCanEqual: Boolean =
912-
sym.isOneOf(GivenOrImplicit) && sym.info.finalResultType.baseClasses.exists(_.derivesFrom(defn.CanEqualClass))
913969
def isMarkerTrait: Boolean =
914970
sym.info.hiBound.resultType.allMembers.forall: d =>
915971
val m = d.symbol
@@ -933,12 +989,6 @@ object CheckUnused:
933989
def boundTpe: Type = sel.bound match
934990
case untpd.TypedSplice(tree) => tree.tpe
935991
case _ => NoType
936-
/** This is used to ignore exclusion imports of the form import `qual.member as _`
937-
* because `sel.isUnimport` is too broad for old style `import concurrent._`.
938-
*/
939-
def isImportExclusion: Boolean = sel.renamed match
940-
case untpd.Ident(nme.WILDCARD) => true
941-
case _ => false
942992

943993
extension (imp: Import)(using Context)
944994
/** Is it the first import clause in a statement? `a.x` in `import a.x, b.{y, z}` */
@@ -949,21 +999,6 @@ object CheckUnused:
949999
def isGeneratedByEnum: Boolean =
9501000
imp.symbol.exists && imp.symbol.owner.is(Enum, butNot = Case)
9511001

952-
/** Under -Wunused:strict-no-implicit-warn, avoid false positives
953-
* if this selector is a wildcard that might import implicits or
954-
* specifically does import an implicit.
955-
* Similarly, import of CanEqual must not warn, as it is always witness.
956-
*/
957-
def isLoose(sel: ImportSelector): Boolean =
958-
if ctx.settings.WunusedHas.strictNoImplicitWarn then
959-
if sel.isWildcard
960-
|| imp.expr.tpe.member(sel.name.toTermName).hasAltWith(_.symbol.isOneOf(GivenOrImplicit))
961-
|| imp.expr.tpe.member(sel.name.toTypeName).hasAltWith(_.symbol.isOneOf(GivenOrImplicit))
962-
then return true
963-
if sel.isWildcard && sel.isGiven
964-
then imp.expr.tpe.allMembers.exists(_.symbol.isCanEqual)
965-
else imp.expr.tpe.member(sel.name.toTermName).hasAltWith(_.symbol.isCanEqual)
966-
9671002
extension (pos: SrcPos)
9681003
def isZeroExtentSynthetic: Boolean = pos.span.isSynthetic && pos.span.isZeroExtent
9691004
def isSynthetic: Boolean = pos.span.isSynthetic && pos.span.exists

tests/pos/i17762.scala

Lines changed: 0 additions & 21 deletions
This file was deleted.

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/i15503j.scala

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//> using options -Wunused:strict-no-implicit-warn
1+
//> using options -Wunused:imports
22

33
package foo.unused.strict.test:
44
package a:
@@ -7,15 +7,15 @@ package foo.unused.strict.test:
77
val z: Int = 2
88
def f: Int = 3
99
package b:
10-
import a.given // OK
11-
import a._ // OK
12-
import a.* // OK
13-
import a.x // OK
14-
import a.y // OK
10+
import a.given // warn
11+
import a._ // warn
12+
import a.* // warn
13+
import a.x // warn
14+
import a.y // warn
1515
import a.z // warn
1616
import a.f // warn
1717
package c:
18-
import a.given // OK
18+
import a.given // warn
1919
import a.x // OK
2020
import a.y // OK
2121
import a.z // OK
@@ -28,8 +28,8 @@ package foo.implicits.resolution:
2828
object A { implicit val x: X = new X }
2929
object B { implicit val y: Y = new Y }
3030
class C {
31-
import A._ // OK
32-
import B._ // OK
31+
import A.given // warn
32+
import B.given // OK
3333
def t = implicitly[X]
3434
}
3535

@@ -44,7 +44,7 @@ package foo.unused.summon.inlines:
4444
given willBeUsed: (A & B) = new A with B {}
4545

4646
package use:
47-
import lib.{A, B, C, willBeUnused, willBeUsed} //OK
47+
import lib.{A, B, C, willBeUnused, willBeUsed} // warn
4848
import compiletime.summonInline //OK
4949

5050
transparent inline given conflictInside: C =
@@ -56,4 +56,4 @@ package foo.unused.summon.inlines:
5656
???
5757

5858
val b: B = summon[B]
59-
val c: C = summon[C]
59+
val c: C = summon[C]

tests/warn/i17762.scala

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//> using options -Wunused:all
2+
3+
class SomeType
4+
5+
def testIt(st1: SomeType, st2: SomeType): Boolean =
6+
given CanEqual[SomeType, SomeType] = CanEqual.derived
7+
st1 == st2
8+
9+
object HasCanEqual:
10+
given f: CanEqual[SomeType, SomeType] = CanEqual.derived
11+
12+
object UsesCanEqual:
13+
import HasCanEqual.given
14+
def testIt(st1: SomeType, st2: SomeType): Boolean =
15+
st1 == st2
16+
17+
object UsesCanEqual2:
18+
import HasCanEqual.f
19+
def testIt(st1: SomeType, st2: SomeType): Boolean =
20+
st1 != st2
21+
22+
object UsesCanEqual3:
23+
import HasCanEqual.f as g
24+
def testIt(st1: SomeType, st2: SomeType): Boolean =
25+
st1 != st2
26+
27+
def warnable(st1: SomeType, st2: SomeType): Boolean =
28+
given CanEqual[SomeType, SomeType] = CanEqual.derived // warn
29+
st1.toString == st2.toString
30+
31+
def importable(st1: SomeType, st2: SomeType): Boolean =
32+
import HasCanEqual.given // warn
33+
st1.toString == st2.toString

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")

tests/warn/unused-can-equal.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
2-
//> using options -Werror -Wunused:all
1+
//> using options -Wunused:all
32

43
import scala.language.strictEquality
54

65
class Box[T](x: T) derives CanEqual:
76
def y = x
87

98
def f[A, B](a: A, b: B)(using CanEqual[A, B]) = a == b // no warn
9+
def z[A, B](a: A, b: B)(using ce: CanEqual[A, B]) = a.toString == b.toString // no warn
1010

1111
def g =
12-
import Box.given // no warn
12+
import Box.given // warn
1313
"42".length
1414

1515
@main def test() = println:

0 commit comments

Comments
 (0)