diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 4c8fea17c076..84253fa63b1f 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -138,6 +138,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case Ident(rename: TermName) => rename case _ => name + /** It's a masking import if `!isWildcard`. */ def isUnimport = rename == nme.WILDCARD } diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index eb3296ab476c..69121c4f51fa 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -193,12 +193,6 @@ private sealed trait WarningSettings: 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:imports, only for imports of explicit named members. - |NOTE : This overrides -Wunused:imports and NOT set by -Wunused:all""".stripMargin - ), - ChoiceWithHelp("unsafe-warn-patvars", "Deprecated alias for `patvars`"), ), default = Nil ) diff --git a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala index e3b6c0daa3d3..fbdd633ef743 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala @@ -206,10 +206,5 @@ class TypeUtils: self.decl(nme.CONSTRUCTOR).altsWith(isApplicable).map(_.symbol) - /** Strip all outer refinements off this type */ - def stripRefinement: Type = self match - case self: RefinedOrRecType => self.parent.stripRefinement - case seld => self - end TypeUtils diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index f0ca272cce36..abdf7c82bbfd 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -6587,23 +6587,24 @@ object Types extends TypeUtils { class TypeSizeAccumulator(using Context) extends TypeAccumulator[Int] { var seen = util.HashSet[Type](initialCapacity = 8) def apply(n: Int, tp: Type): Int = - if seen.contains(tp) then n - else { - seen += tp - tp match { - case tp: AppliedType => - val tpNorm = tp.tryNormalize - if tpNorm.exists then apply(n, tpNorm) - else foldOver(n + 1, tp) - case tp: RefinedType => - foldOver(n + 1, tp) - case tp: TypeRef if tp.info.isTypeAlias => - apply(n, tp.superType) - case tp: TypeParamRef => - apply(n, TypeComparer.bounds(tp)) - case _ => + tp match { + case tp: AppliedType => + val tpNorm = tp.tryNormalize + if tpNorm.exists then apply(n, tpNorm) + else foldOver(n + 1, tp) + case tp: RefinedType => + foldOver(n + 1, tp) + case tp: TypeRef if tp.info.isTypeAlias => + apply(n, tp.superType) + case tp: TypeParamRef => + apply(n, TypeComparer.bounds(tp)) + case tp: LazyRef => + if seen.contains(tp) then n + else + seen += tp foldOver(n, tp) - } + case _ => + foldOver(n, tp) } } diff --git a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala index 1ec68d4e2db4..03d0f0dd1e8d 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala @@ -26,6 +26,7 @@ import dotty.tools.dotc.util.chaining.* import java.util.IdentityHashMap +import scala.annotation.* import scala.collection.mutable, mutable.{ArrayBuilder, ListBuffer, Stack} import CheckUnused.* @@ -288,6 +289,8 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha alt.symbol == sym || nm.isTypeName && alt.symbol.isAliasType && alt.info.dealias.typeSymbol == sym sameSym && alt.symbol.isAccessibleFrom(qtpe) + def hasAltMemberNamed(nm: Name) = qtpe.member(nm).hasAltWith(_.symbol.isAccessibleFrom(qtpe)) + def loop(sels: List[ImportSelector]): ImportSelector | Null = sels match case sel :: sels => val matches = @@ -304,9 +307,17 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha else !sym.is(Given) // Normal wildcard, check that the symbol is not a given (but can be implicit) } + else if sel.isUnimport then + val masksMatchingMember = + name != nme.NO_NAME + && sels.exists(x => x.isWildcard && !x.isGiven) + && !name.exists(_.toTermName != sel.name) // import a.b as _, b must match name + && (hasAltMemberNamed(sel.name) || hasAltMemberNamed(sel.name.toTypeName)) + if masksMatchingMember then + refInfos.sels.put(sel, ()) // imprecise due to precedence but errs on the side of false negative + false else - // if there is an explicit name, it must match - !name.exists(_.toTermName != sel.rename) + !name.exists(_.toTermName != sel.rename) // if there is an explicit name, it must match && (prefix.eq(NoPrefix) || qtpe =:= prefix) && (hasAltMember(sel.name) || hasAltMember(sel.name.toTypeName)) if matches then sel else loop(sels) @@ -639,7 +650,6 @@ object CheckUnused: || m.is(Synthetic) || m.hasAnnotation(dd.UnusedAnnot) // param of unused method || sym.owner.name.isContextFunction // a ubiquitous parameter - || sym.isCanEqual || sym.info.dealias.typeSymbol.match // more ubiquity case dd.DummyImplicitClass | dd.SubTypeClass | dd.SameTypeClass => true case tps => @@ -671,7 +681,6 @@ object CheckUnused: def checkLocal(sym: Symbol, pos: SrcPos) = if ctx.settings.WunusedHas.locals && !sym.is(InlineProxy) - && !sym.isCanEqual then if sym.is(Mutable) && infos.asss(sym) then warnAt(pos)(UnusedSymbol.localVars) @@ -699,7 +708,6 @@ object CheckUnused: warnAt(pos)(UnusedSymbol.unsetPrivates) def checkImports() = - // TODO check for unused masking import import scala.jdk.CollectionConverters.given import Rewrites.ActionPatch type ImpSel = (Import, ImportSelector) @@ -712,7 +720,7 @@ object CheckUnused: warnAt(sel.srcPos)(msg, origin) if !actionable then - for imp <- infos.imps.keySet.nn.asScala; sel <- imp.selectors if !isUsable(imp, sel) do + for imp <- infos.imps.keySet.nn.asScala; sel <- imp.selectors if !isUsed(sel) do warnImport(imp -> sel) else // If the rest of the line is blank, include it in the final edit position. (Delete trailing whitespace.) @@ -767,7 +775,7 @@ object CheckUnused: 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 + imp.selectors.exists(!isUsed(_)) // 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). @@ -776,7 +784,7 @@ object CheckUnused: // 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(_, _)) + .partition((imp, sel) => isUsed(sel)) if keeping.isEmpty then val editPos = existing.head.srcPos.sourcePos.withSpan: Span(start = existing.head.srcPos.span.start, end = existing.last.srcPos.span.end) @@ -978,8 +986,6 @@ object CheckUnused: 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.info.hiBound.resultType.allMembers.forall: d => val m = d.symbol @@ -1003,6 +1009,11 @@ object CheckUnused: def boundTpe: Type = sel.bound match case untpd.TypedSplice(tree) => tree.tpe case _ => NoType + /** Is a "masking" import of the form import `qual.member as _`. + * Both conditions must be checked. + */ + @unused // matchingSelector checks isWildcard first + def isImportExclusion: Boolean = sel.isUnimport && !sel.isWildcard extension (imp: Import)(using Context) /** Is it the first import clause in a statement? `a.x` in `import a.x, b.{y, z}` */ @@ -1013,21 +1024,6 @@ object CheckUnused: def isGeneratedByEnum: 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): 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 def isSynthetic: Boolean = pos.span.isSynthetic && pos.span.exists diff --git a/tests/neg/i15692.scala b/tests/neg/i15692.scala new file mode 100644 index 000000000000..0cb163426691 --- /dev/null +++ b/tests/neg/i15692.scala @@ -0,0 +1,5 @@ +trait TC[X] +object TC { + given [T, S <: TC[S]](using TC[S]): TC[T] = ??? + summon[TC[Int]] // error +} diff --git a/tests/pos/i15692.scala b/tests/pos/i15692.scala new file mode 100644 index 000000000000..99eddcd33d71 --- /dev/null +++ b/tests/pos/i15692.scala @@ -0,0 +1,24 @@ +sealed trait Nat +sealed trait Succ[Prev <: Nat] extends Nat +sealed trait Zero extends Nat + +class Sum[M <: Nat, N <: Nat] { + type Out <: Nat +} + +object Sum { + type Aux[M <: Nat, N <: Nat, R <: Nat] = Sum[M, N] { type Out = R } + + implicit def sum0[N <: Nat]: Sum.Aux[Zero, N, N] = new Sum[Zero, N] { type Out = N } + implicit def sum1[M <: Nat, N <: Nat, R <: Nat](implicit sum: Sum.Aux[M, Succ[N], R]): Sum.Aux[Succ[M], N, R] = + new Sum[Succ[M], N] { type Out = R } +} + +object Test { + def main(args: Array[String]): Unit = { + type _3 = Succ[Succ[Succ[Zero]]] + type _5 = Succ[Succ[_3]] + + implicitly[Sum[_3, _5]] + } +} diff --git a/tests/warn/i15503a.scala b/tests/warn/i15503a.scala index 8fc97888b584..439799ee8e3d 100644 --- a/tests/warn/i15503a.scala +++ b/tests/warn/i15503a.scala @@ -85,12 +85,12 @@ object InnerMostCheck: val a = Set(1) object IgnoreExclusion: - import collection.mutable.{Set => _} // OK - import collection.mutable.{Map => _} // OK + import collection.mutable.{Map => _, Set => _, *} // OK?? import collection.mutable.{ListBuffer} // warn def check = val a = Set(1) val b = Map(1 -> 2) + def c = Seq(42) /** * Some given values for the test */ diff --git a/tests/warn/i15503j.scala b/tests/warn/i15503j.scala index fa30601d8960..62e3557fc0d3 100644 --- a/tests/warn/i15503j.scala +++ b/tests/warn/i15503j.scala @@ -1,4 +1,4 @@ -//> using options -Wunused:strict-no-implicit-warn +//> using options -Wunused:imports package foo.unused.strict.test: package a: @@ -7,15 +7,15 @@ package foo.unused.strict.test: val z: Int = 2 def f: Int = 3 package b: - import a.given // OK - import a._ // OK - import a.* // OK - import a.x // OK - import a.y // OK + import a.given // warn + import a._ // warn + import a.* // warn + import a.x // warn + import a.y // warn import a.z // warn import a.f // warn package c: - import a.given // OK + import a.given // warn import a.x // OK import a.y // OK import a.z // OK @@ -28,8 +28,8 @@ package foo.implicits.resolution: object A { implicit val x: X = new X } object B { implicit val y: Y = new Y } class C { - import A._ // OK - import B._ // OK + import A.given // warn + import B.given // OK def t = implicitly[X] } @@ -44,7 +44,7 @@ package foo.unused.summon.inlines: given willBeUsed: (A & B) = new A with B {} package use: - import lib.{A, B, C, willBeUnused, willBeUsed} //OK + import lib.{A, B, C, willBeUnused, willBeUsed} // warn import compiletime.summonInline //OK transparent inline given conflictInside: C = @@ -56,4 +56,4 @@ package foo.unused.summon.inlines: ??? val b: B = summon[B] - val c: C = summon[C] \ No newline at end of file + val c: C = summon[C] diff --git a/tests/pos/i17762.scala b/tests/warn/i17762.scala similarity index 87% rename from tests/pos/i17762.scala rename to tests/warn/i17762.scala index 65275c4619db..e3adda821fad 100644 --- a/tests/pos/i17762.scala +++ b/tests/warn/i17762.scala @@ -1,4 +1,4 @@ -//> using options -Xfatal-warnings -Wunused:all +//> using options -Werror -Wunused:all class SomeType @@ -16,6 +16,5 @@ object UsesCanEqual: object UsesCanEqual2: import HasCanEqual.f - def testIt(st1: SomeType, st2: SomeType): Boolean = - st1 == st2 \ No newline at end of file + st1 != st2 diff --git a/tests/warn/i23758.scala b/tests/warn/i23758.scala new file mode 100644 index 000000000000..08acb8aa588c --- /dev/null +++ b/tests/warn/i23758.scala @@ -0,0 +1,11 @@ +//> using options -Wunused:imports + +import scala.util.Try as _ // warn + +class Promise(greeting: String): + override def toString = greeting + +@main def test = println: + import scala.concurrent.{Promise as _, *}, ExecutionContext.Implicits.given + val promise = new Promise("world") + Future(s"hello, $promise") diff --git a/tests/warn/unused-can-equal.scala b/tests/warn/unused-can-equal.scala index 6e38591ccef1..7c7f0a61e163 100644 --- a/tests/warn/unused-can-equal.scala +++ b/tests/warn/unused-can-equal.scala @@ -1,5 +1,4 @@ - -//> using options -Werror -Wunused:all +//> using options -Wunused:all import scala.language.strictEquality @@ -7,9 +6,10 @@ 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 z[A, B](a: A, b: B)(using ce: CanEqual[A, B]) = a.toString == b.toString // no warn def g = - import Box.given // no warn + import Box.given // warn "42".length @main def test() = println: