diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index d33b2c574318..85d470fdf0f2 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -3277,11 +3277,10 @@ extends Message(UnusedSymbolID) { override def explain(using Context) = "" } -object UnusedSymbol { - def imports(using Context): UnusedSymbol = new UnusedSymbol(i"unused import") - def localDefs(using Context): UnusedSymbol = new UnusedSymbol(i"unused local definition") - def explicitParams(using Context): UnusedSymbol = new UnusedSymbol(i"unused explicit parameter") - def implicitParams(using Context): UnusedSymbol = new UnusedSymbol(i"unused implicit parameter") - def privateMembers(using Context): UnusedSymbol = new UnusedSymbol(i"unused private member") - def patVars(using Context): UnusedSymbol = new UnusedSymbol(i"unused pattern variable") -} +object UnusedSymbol: + def imports(using Context): UnusedSymbol = UnusedSymbol(i"unused import") + def localDefs(using Context): UnusedSymbol = UnusedSymbol(i"unused local definition") + def explicitParams(using Context): UnusedSymbol = UnusedSymbol(i"unused explicit parameter") + def implicitParams(using Context): UnusedSymbol = UnusedSymbol(i"unused implicit parameter") + def privateMembers(using Context): UnusedSymbol = UnusedSymbol(i"unused private member") + def patVars(using Context): UnusedSymbol = UnusedSymbol(i"unused pattern variable") diff --git a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala index b438c2398983..0fc29713f604 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala @@ -10,7 +10,7 @@ import dotty.tools.dotc.ast.untpd import dotty.tools.dotc.ast.untpd.ImportSelector import dotty.tools.dotc.config.ScalaSettings import dotty.tools.dotc.core.Contexts.* -import dotty.tools.dotc.core.Decorators.{em, i} +import dotty.tools.dotc.core.Decorators.{em, i, toMessage} import dotty.tools.dotc.core.Denotations.SingleDenotation import dotty.tools.dotc.core.Flags.* import dotty.tools.dotc.core.Phases.Phase @@ -33,7 +33,6 @@ import dotty.tools.dotc.core.NameKinds.WildcardParamName import dotty.tools.dotc.core.Symbols.Symbol import dotty.tools.dotc.core.StdNames.nme import dotty.tools.dotc.util.Spans.Span -import scala.math.Ordering import scala.util.chaining.given /** A compiler phase that checks for unused imports or definitions. @@ -45,10 +44,12 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke import CheckUnused.* import UnusedData.* - private inline def unusedDataApply[U](inline f: UnusedData => U)(using Context): Context = + private inline def ud(using ud: UnusedData): UnusedData = ud + + private inline def go[U](inline op: UnusedData ?=> U)(using ctx: Context): ctx.type = ctx.property(_key) match - case Some(ud) => f(ud) - case None => () + case Some(ud) => op(using ud) + case None => ctx override def phaseName: String = CheckUnused.phaseNamePrefix + suffix @@ -72,11 +73,10 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke // ========== END + REPORTING ========== override def transformUnit(tree: tpd.Tree)(using Context): tpd.Tree = - unusedDataApply { ud => + go: ud.finishAggregation() if phaseMode == PhaseMode.Report then ud.unusedAggregate.foreach(reportUnused) - } tree // ========== MiniPhase Prepare ========== @@ -89,28 +89,22 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke traverser.traverse(tree.call) ctx - override def prepareForIdent(tree: tpd.Ident)(using Context): Context = + override def prepareForIdent(tree: tpd.Ident)(using Context): Context = go: if tree.symbol.exists then - unusedDataApply { ud => - @tailrec - def loopOnNormalizedPrefixes(prefix: Type, depth: Int): Unit = - // limit to 10 as failsafe for the odd case where there is an infinite cycle - if depth < 10 && prefix.exists then - ud.registerUsed(prefix.classSymbol, None, prefix) - loopOnNormalizedPrefixes(prefix.normalizedPrefix, depth + 1) - - val prefix = tree.typeOpt.normalizedPrefix - loopOnNormalizedPrefixes(prefix, depth = 0) - ud.registerUsed(tree.symbol, Some(tree.name), prefix) - } + def loopOnNormalizedPrefixes(prefix: Type, depth: Int): Unit = + // limit to 10 as failsafe for the odd case where there is an infinite cycle + if depth < 10 && prefix.exists then + ud.registerUsed(prefix.classSymbol, None, prefix) + loopOnNormalizedPrefixes(prefix.normalizedPrefix, depth + 1) + val prefix = tree.typeOpt.normalizedPrefix + loopOnNormalizedPrefixes(prefix, depth = 0) + ud.registerUsed(tree.symbol, Some(tree.name), prefix) else if tree.hasType then - unusedDataApply(_.registerUsed(tree.tpe.classSymbol, Some(tree.name), tree.tpe.normalizedPrefix)) - else - ctx + ud.registerUsed(tree.tpe.classSymbol, Some(tree.name), tree.tpe.normalizedPrefix) - override def prepareForSelect(tree: tpd.Select)(using Context): Context = + override def prepareForSelect(tree: tpd.Select)(using Context): Context = go: val name = tree.removeAttachment(OriginalName) - unusedDataApply(_.registerUsed(tree.symbol, name, tree.qualifier.tpe, includeForImport = tree.qualifier.span.isSynthetic)) + ud.registerUsed(tree.symbol, name, tree.qualifier.tpe, includeForImport = tree.qualifier.span.isSynthetic) override def prepareForBlock(tree: tpd.Block)(using Context): Context = pushInBlockTemplatePackageDef(tree) @@ -121,52 +115,41 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke override def prepareForPackageDef(tree: tpd.PackageDef)(using Context): Context = pushInBlockTemplatePackageDef(tree) - override def prepareForValDef(tree: tpd.ValDef)(using Context): Context = - unusedDataApply{ud => - traverseAnnotations(tree.symbol) - // do not register the ValDef generated for `object` - if !tree.symbol.is(Module) then - ud.registerDef(tree) - if tree.name.startsWith("derived$") && tree.hasType then - ud.registerUsed(tree.tpe.typeSymbol, None, tree.tpe.normalizedPrefix, isDerived = true) - ud.addIgnoredUsage(tree.symbol) - } + override def prepareForValDef(tree: tpd.ValDef)(using Context): Context = go: + traverseAnnotations(tree.symbol) + // do not register the ValDef generated for `object` + if !tree.symbol.is(Module) then + ud.registerDef(tree) + if tree.name.startsWith("derived$") && tree.hasType then + ud.registerUsed(tree.tpe.typeSymbol, None, tree.tpe.normalizedPrefix, isDerived = true) + ud.addIgnoredUsage(tree.symbol) + + override def prepareForDefDef(tree: tpd.DefDef)(using Context): Context = go: + if !tree.symbol.is(Private) then + tree.termParamss.flatten.foreach(p => ud.addIgnoredParam(p.symbol)) + ud.registerTrivial(tree) + traverseAnnotations(tree.symbol) + ud.registerDef(tree) + ud.addIgnoredUsage(tree.symbol) - override def prepareForDefDef(tree: tpd.DefDef)(using Context): Context = - unusedDataApply{ ud => - if !tree.symbol.is(Private) then - tree.termParamss.flatten.foreach { p => - ud.addIgnoredParam(p.symbol) - } - import ud.registerTrivial - tree.registerTrivial + override def prepareForTypeDef(tree: tpd.TypeDef)(using Context): Context = go: + if !tree.symbol.is(Param) then // Ignore type parameter (as Scala 2) traverseAnnotations(tree.symbol) ud.registerDef(tree) ud.addIgnoredUsage(tree.symbol) - } - override def prepareForTypeDef(tree: tpd.TypeDef)(using Context): Context = - unusedDataApply{ ud => - if !tree.symbol.is(Param) then // Ignore type parameter (as Scala 2) - traverseAnnotations(tree.symbol) - ud.registerDef(tree) - ud.addIgnoredUsage(tree.symbol) - } - - override def prepareForBind(tree: tpd.Bind)(using Context): Context = + override def prepareForBind(tree: tpd.Bind)(using Context): Context = go: traverseAnnotations(tree.symbol) - unusedDataApply(_.registerPatVar(tree)) + ud.registerPatVar(tree) override def prepareForTypeTree(tree: tpd.TypeTree)(using Context): Context = - if !tree.isInstanceOf[tpd.InferredTypeTree] then typeTraverser(unusedDataApply).traverse(tree.tpe) + if !tree.isInstanceOf[tpd.InferredTypeTree] then typeTraverser.traverse(tree.tpe) ctx - override def prepareForAssign(tree: tpd.Assign)(using Context): Context = - unusedDataApply{ ud => - val sym = tree.lhs.symbol - if sym.exists then - ud.registerSetVar(sym) - } + override def prepareForAssign(tree: tpd.Assign)(using Context): Context = go: + val sym = tree.lhs.symbol + if sym.exists then + ud.registerSetVar(sym) // ========== MiniPhase Transform ========== @@ -183,31 +166,25 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke tree override def transformValDef(tree: tpd.ValDef)(using Context): tpd.Tree = - unusedDataApply(_.removeIgnoredUsage(tree.symbol)) + go(ud.removeIgnoredUsage(tree.symbol)) tree override def transformDefDef(tree: tpd.DefDef)(using Context): tpd.Tree = - unusedDataApply(_.removeIgnoredUsage(tree.symbol)) + go(ud.removeIgnoredUsage(tree.symbol)) tree override def transformTypeDef(tree: tpd.TypeDef)(using Context): tpd.Tree = - unusedDataApply(_.removeIgnoredUsage(tree.symbol)) + go(ud.removeIgnoredUsage(tree.symbol)) tree // ---------- MiniPhase HELPERS ----------- - private def pushInBlockTemplatePackageDef(tree: tpd.Block | tpd.Template | tpd.PackageDef)(using Context): Context = - unusedDataApply { ud => - ud.pushScope(UnusedData.ScopeType.fromTree(tree)) - } - ctx + private def pushInBlockTemplatePackageDef(tree: tpd.Block | tpd.Template | tpd.PackageDef)(using Context): Context = go: + ud.pushScope(UnusedData.ScopeType.fromTree(tree)) - private def popOutBlockTemplatePackageDef()(using Context): Context = - unusedDataApply { ud => - ud.popScope() - } - ctx + private def popOutBlockTemplatePackageDef()(using Context): Context = go: + ud.popScope() /** * This traverse is the **main** component of this phase @@ -224,7 +201,7 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke val newCtx = if tree.symbol.exists then ctx.withOwner(tree.symbol) else ctx tree match case imp: tpd.Import => - unusedDataApply(_.registerImport(imp)) + go(ud.registerImport(imp)) imp.selectors.filter(_.isGiven).map(_.bound).collect { case untpd.TypedSplice(tree1) => tree1 }.foreach(traverse(_)(using newCtx)) @@ -261,11 +238,11 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke case _: tpd.InferredTypeTree => case t@tpd.RefinedTypeTree(tpt, refinements) => //! DIFFERS FROM MINIPHASE - typeTraverser(unusedDataApply).traverse(t.tpe) + typeTraverser.traverse(t.tpe) traverse(tpt)(using newCtx) case t@tpd.TypeTree() => //! DIFFERS FROM MINIPHASE - typeTraverser(unusedDataApply).traverse(t.tpe) + typeTraverser.traverse(t.tpe) traverseChildren(tree)(using newCtx) case _ => //! DIFFERS FROM MINIPHASE @@ -274,12 +251,12 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke end traverser /** This is a type traverser which catch some special Types not traversed by the term traverser above */ - private def typeTraverser(dt: (UnusedData => Any) => Unit)(using Context) = new TypeTraverser: + private def typeTraverser(using Context) = new TypeTraverser: override def traverse(tp: Type): Unit = - if tp.typeSymbol.exists then dt(_.registerUsed(tp.typeSymbol, Some(tp.typeSymbol.name), tp.normalizedPrefix)) + if tp.typeSymbol.exists then go(ud.registerUsed(tp.typeSymbol, Some(tp.typeSymbol.name), tp.normalizedPrefix)) tp match case AnnotatedType(_, annot) => - dt(_.registerUsed(annot.symbol, None, annot.symbol.info.normalizedPrefix)) + go(ud.registerUsed(annot.symbol, None, annot.symbol.info.normalizedPrefix)) traverseChildren(tp) case _ => traverseChildren(tp) @@ -288,26 +265,22 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke private def traverseAnnotations(sym: Symbol)(using Context): Unit = sym.denot.annotations.foreach(annot => traverser.traverse(annot.tree)) - /** Do the actual reporting given the result of the anaylsis */ private def reportUnused(res: UnusedData.UnusedResult)(using Context): Unit = + def messageFor(w: WarnTypes): Message = + import WarnTypes.*, UnusedSymbolMessage.* + w match + case Imports => imports + case LocalDefs => localDefs + case ExplicitParams => explicitParams + case ImplicitParams => implicitParams + case PrivateMembers => privateMembers + case PatVars => patVars + case UnsetLocals => "unset local variable, consider using an immutable val instead".toMessage + case UnsetPrivates => "unset private variable, consider using an immutable val instead".toMessage res.warnings.toArray.sortInPlaceBy(_.pos.span.point).foreach: - case UnusedSymbol(t, _, WarnTypes.Imports) => - report.warning(UnusedSymbolMessage.imports, t) - case UnusedSymbol(t, _, WarnTypes.LocalDefs) => - report.warning(UnusedSymbolMessage.localDefs, t) - case UnusedSymbol(t, _, WarnTypes.ExplicitParams) => - report.warning(UnusedSymbolMessage.explicitParams, t) - case UnusedSymbol(t, _, WarnTypes.ImplicitParams) => - report.warning(UnusedSymbolMessage.implicitParams, t) - case UnusedSymbol(t, _, WarnTypes.PrivateMembers) => - report.warning(UnusedSymbolMessage.privateMembers, t) - case UnusedSymbol(t, _, WarnTypes.PatVars) => - report.warning(UnusedSymbolMessage.patVars, t) - case UnusedSymbol(t, _, WarnTypes.UnsetLocals) => - report.warning("unset local variable, consider using an immutable val instead", t) - case UnusedSymbol(t, _, WarnTypes.UnsetPrivates) => - report.warning("unset private variable, consider using an immutable val instead", t) + case UnusedSymbol(pos, _, warnType) => + report.warning(messageFor(warnType), pos) end CheckUnused @@ -390,6 +363,10 @@ object CheckUnused: case Some(prevUnused) => Some(UnusedResult(getUnused.warnings.intersect(prevUnused.warnings))) + def registerSelectors(selectors: List[ImportSelectorData]): this.type = + impInScope.top.prependAll(selectors) + this + /** * Register a found (used) symbol along with its name * @@ -447,11 +424,12 @@ object CheckUnused: val newDataInScope = for sel <- reorderedSelectors yield val data = new ImportSelectorData(qualTpe, sel) - if shouldSelectorBeReported(imp, sel) || isImportExclusion(sel) || isImportIgnored(imp, sel) then + assert(sel.isUnimport == isImportExclusion(sel), sel) + if shouldSelectorBeReported(imp, sel) || sel.isUnimport || isImportIgnored(imp, sel) then // Immediately mark the selector as used data.markUsed() data - impInScope.top.prependAll(newDataInScope) + registerSelectors(newDataInScope) end registerImport /** Register (or not) some `val` or `def` according to the context, scope and flags */ @@ -509,12 +487,8 @@ object CheckUnused: unusedImport += selData end popScope - /** - * Leave the scope and return a `List` of unused `ImportSelector`s - * - * The given `List` is sorted by line and then column of the position + /** Leave the scope and return a result set of warnings. */ - def getUnused(using Context): UnusedResult = popScope() @@ -523,69 +497,47 @@ object CheckUnused: case Some(syms) => syms.exists(sym => span.contains(sym.span)) case None => false - val sortedImp = - if ctx.settings.WunusedHas.imports || ctx.settings.WunusedHas.strictNoImplicitWarn then - unusedImport.toList - .map(d => UnusedSymbol(d.selector.srcPos, d.selector.name, WarnTypes.Imports)) - else - Nil + val warnings = Set.newBuilder[UnusedSymbol] + + if ctx.settings.WunusedHas.imports || ctx.settings.WunusedHas.strictNoImplicitWarn then + warnings.addAll(unusedImport.iterator.map(d => UnusedSymbol(d.selector.srcPos, d.selector.name, WarnTypes.Imports))) + // Partition to extract unset local variables from usedLocalDefs - val (usedLocalDefs, unusedLocalDefs) = - if ctx.settings.WunusedHas.locals then - localDefInScope.toList.partition(d => d.symbol.usedDefContains) - else - (Nil, Nil) - val sortedLocalDefs = - unusedLocalDefs - .filterNot(d => isUsedInPosition(d.symbol.name, d.span)) - .filterNot(d => containsSyntheticSuffix(d.symbol)) - .map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.LocalDefs)) - val unsetLocalDefs = usedLocalDefs.filter(isUnsetVarDef).map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.UnsetLocals)).toList - - val sortedExplicitParams = - if ctx.settings.WunusedHas.explicits then - explicitParamInScope.toList - .filterNot(d => d.symbol.usedDefContains) - .filterNot(d => isUsedInPosition(d.symbol.name, d.span)) - .filterNot(d => containsSyntheticSuffix(d.symbol)) - .map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.ExplicitParams)) - else - Nil - val sortedImplicitParams = - if ctx.settings.WunusedHas.implicits then - implicitParamInScope.toList - .filterNot(d => d.symbol.usedDefContains) - .filterNot(d => containsSyntheticSuffix(d.symbol)) - .map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.ImplicitParams)) - else - Nil + if ctx.settings.WunusedHas.locals then + for d <- localDefInScope do + if d.symbol.usedDefContains then + if isUnsetVarDef(d) then + warnings.addOne(UnusedSymbol(d.namePos, d.name, WarnTypes.UnsetLocals)) + else + if !isUsedInPosition(d.symbol.name, d.span) && !containsSyntheticSuffix(d.symbol) then + warnings.addOne(UnusedSymbol(d.namePos, d.name, WarnTypes.LocalDefs)) + + if ctx.settings.WunusedHas.explicits then + for d <- explicitParamInScope do + if !d.symbol.usedDefContains && !isUsedInPosition(d.symbol.name, d.span) && !containsSyntheticSuffix(d.symbol) then + warnings.addOne(UnusedSymbol(d.namePos, d.name, WarnTypes.ExplicitParams)) + + if ctx.settings.WunusedHas.implicits then + for d <- implicitParamInScope do + if !d.symbol.usedDefContains && !containsSyntheticSuffix(d.symbol) then + warnings.addOne(UnusedSymbol(d.namePos, d.name, WarnTypes.ImplicitParams)) + // Partition to extract unset private variables from usedPrivates - val (usedPrivates, unusedPrivates) = - if ctx.settings.WunusedHas.privates then - privateDefInScope.toList.partition(d => d.symbol.usedDefContains) - else - (Nil, Nil) - val sortedPrivateDefs = unusedPrivates.filterNot(d => containsSyntheticSuffix(d.symbol)).map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.PrivateMembers)) - val unsetPrivateDefs = usedPrivates.filter(isUnsetVarDef).map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.UnsetPrivates)) - val sortedPatVars = - if ctx.settings.WunusedHas.patvars then - patVarsInScope.toList - .filterNot(d => d.symbol.usedDefContains) - .filterNot(d => containsSyntheticSuffix(d.symbol)) - .filterNot(d => isUsedInPosition(d.symbol.name, d.span)) - .map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.PatVars)) - else - Nil - val warnings = - sortedImp ::: - sortedLocalDefs ::: - sortedExplicitParams ::: - sortedImplicitParams ::: - sortedPrivateDefs ::: - sortedPatVars ::: - unsetLocalDefs ::: - unsetPrivateDefs - UnusedResult(warnings.toSet) + if ctx.settings.WunusedHas.privates then + for d <- privateDefInScope do + if d.symbol.usedDefContains then + if isUnsetVarDef(d) then + warnings.addOne(UnusedSymbol(d.namePos, d.name, WarnTypes.UnsetPrivates)) + else + if !containsSyntheticSuffix(d.symbol) then + warnings.addOne(UnusedSymbol(d.namePos, d.name, WarnTypes.PrivateMembers)) + + if ctx.settings.WunusedHas.patvars then + for d <- patVarsInScope do + if !d.symbol.usedDefContains && !isUsedInPosition(d.symbol.name, d.span) && !containsSyntheticSuffix(d.symbol) then + warnings.addOne(UnusedSymbol(d.namePos, d.name, WarnTypes.PatVars)) + + UnusedResult(warnings.result) end getUnused //============================ HELPERS ==================================== @@ -597,7 +549,7 @@ object CheckUnused: imp.selectors.exists { sel => val qual = imp.expr val importedMembers = qual.tpe.member(sel.name).alternatives.map(_.symbol) - importedMembers.exists(s => s.is(Transparent) && s.is(Inline)) + importedMembers.exists(_.isAllOf(Transparent | Inline)) } /** @@ -638,9 +590,7 @@ object CheckUnused: private def isSyntheticMainParam(sym: Symbol)(using Context): Boolean = sym.exists && ctx.platform.isMainMethod(sym.owner) && sym.owner.is(Synthetic) - /** - * This is used to ignore exclusion imports (i.e. import `qual`.{`member` => _}) - */ + /** This is used to ignore exclusion imports (i.e. import `qual`.{`member` => _}) */ private def isImportExclusion(sel: ImportSelector): Boolean = sel.renamed match case untpd.Ident(name) => name == StdNames.nme.WILDCARD case _ => false @@ -856,5 +806,4 @@ object CheckUnused: symbol.asType.typeRef.dealias.typeSymbol else symbol - end CheckUnused