@@ -13,10 +13,12 @@ import dotty.tools.dotc.core.StdNames.nme
1313import dotty .tools .dotc .core .Symbols .{ClassSymbol , NoSymbol , Symbol , defn , isDeprecated , requiredClass , requiredModule }
1414import dotty .tools .dotc .core .Types .*
1515import dotty .tools .dotc .report
16- import dotty .tools .dotc .reporting .UnusedSymbol
16+ import dotty .tools .dotc .reporting .{CodeAction , UnusedSymbol }
17+ import dotty .tools .dotc .rewrites .Rewrites
1718import dotty .tools .dotc .transform .MegaPhase .MiniPhase
1819import dotty .tools .dotc .typer .ImportInfo
19- import dotty .tools .dotc .util .{Property , SrcPos }
20+ import dotty .tools .dotc .util .{Property , Spans , SrcPos }, Spans .Span
21+ import dotty .tools .dotc .util .Chars .{isLineBreakChar , isWhitespace }
2022import dotty .tools .dotc .util .chaining .*
2123
2224import java .util .IdentityHashMap
@@ -411,7 +413,6 @@ object CheckUnused:
411413 case imp : Import =>
412414 if languageImport(imp.expr).isEmpty
413415 && ! imp.isGeneratedByEnum
414- // && !imp.isTransparentInline
415416 then
416417 imps.put(imp, ())
417418 case tree : Bind =>
@@ -436,26 +437,34 @@ object CheckUnused:
436437 def saw (sym : Symbol , name : Name , pre : Type )(using Context ): Boolean =
437438 seen(sym).exists((n, p) => n == name && p =:= pre)
438439
439- def reportUnused ()(using Context ): Unit = warnings.foreach(report.warning(_, _))
440+ def reportUnused ()(using Context ): Unit =
441+ for (msg, pos, origin) <- warnings do
442+ if origin.isEmpty then report.warning(msg, pos)
443+ else report.warning(msg, pos, origin)
444+ msg.actions.headOption.foreach(Rewrites .applyAction)
440445
441- def warnings (using Context ): Array [(UnusedSymbol , SrcPos )] =
442- val warnings = ArrayBuilder .make[(UnusedSymbol , SrcPos )]
446+ type MessageInfo = (UnusedSymbol , SrcPos , String ) // string is origin or empty
447+
448+ def warnings (using Context ): Array [MessageInfo ] =
449+ val actionable = ctx.settings.rewrite.value.nonEmpty
450+ val warnings = ArrayBuilder .make[MessageInfo ]
451+ def warnAt (pos : SrcPos )(msg : UnusedSymbol , origin : String = " " ): Unit = warnings.addOne((msg, pos, origin))
443452 val infos = refInfos
444453 for (sym, pos) <- infos.defs.iterator if ! sym.hasAnnotation(defn.UnusedAnnot ) do
445454 if infos.refs(sym) then
446455 if sym.isLocalToBlock then
447456 if ctx.settings.WunusedHas .locals && sym.is(Mutable ) && ! infos.asss(sym) then
448- warnings.addOne( (UnusedSymbol .unsetLocals, pos) )
457+ warnAt(pos) (UnusedSymbol .unsetLocals)
449458 else if sym.isAllOf(Private | Mutable ) && ! infos.asss(sym) then
450- warnings.addOne( (UnusedSymbol .unsetPrivates, pos) )
459+ warnAt(pos) (UnusedSymbol .unsetPrivates)
451460 else if sym.is(Private , butNot = ParamAccessor ) then
452461 if ctx.settings.WunusedHas .privates
453462 && ! sym.isConstructor
454463 && sym.is(Private , butNot = SelfName | Synthetic | CaseAccessor )
455464 && ! sym.isSerializationSupport
456465 && ! (sym.is(Mutable ) && sym.isSetter && sym.owner.is(Trait )) // tracks sym.underlyingSymbol sibling getter
457466 then
458- warnings.addOne( (UnusedSymbol .privateMembers, pos) )
467+ warnAt(pos) (UnusedSymbol .privateMembers)
459468 else if sym.is(Param , butNot = Given | Implicit ) then
460469 val m = sym.owner
461470 def forgiven (sym : Symbol ) =
@@ -476,10 +485,10 @@ object CheckUnused:
476485 if aliasSym.isAllOf(PrivateParamAccessor , butNot = CaseAccessor ) && ! infos.refs(alias.symbol) then
477486 if aliasSym.is(Local ) then
478487 if ctx.settings.WunusedHas .explicits then
479- warnings.addOne( (UnusedSymbol .explicitParams, pos) )
488+ warnAt(pos) (UnusedSymbol .explicitParams)
480489 else
481490 if ctx.settings.WunusedHas .privates then
482- warnings.addOne( (UnusedSymbol .privateMembers, pos) )
491+ warnAt(pos) (UnusedSymbol .privateMembers)
483492 else if ctx.settings.WunusedHas .explicits
484493 && ! sym.is(Synthetic ) // param to setter is unused bc there is no field yet
485494 && ! (sym.owner.is(ExtensionMethod ) && {
@@ -490,7 +499,7 @@ object CheckUnused:
490499 && ! sym.name.isInstanceOf [DerivedName ]
491500 && ! ctx.platform.isMainMethod(m)
492501 then
493- warnings.addOne( (UnusedSymbol .explicitParams, pos) )
502+ warnAt(pos) (UnusedSymbol .explicitParams)
494503 end checkExplicit
495504 if ! infos.skip(m)
496505 && ! forgiven(sym)
@@ -519,14 +528,14 @@ object CheckUnused:
519528 if alias.exists then
520529 val aliasSym = alias.symbol
521530 if aliasSym.is(ParamAccessor ) && ! infos.refs(alias.symbol) then
522- warnings.addOne( (UnusedSymbol .implicitParams, pos) )
531+ warnAt(pos) (UnusedSymbol .implicitParams)
523532 else
524- warnings.addOne( (UnusedSymbol .implicitParams, pos) )
533+ warnAt(pos) (UnusedSymbol .implicitParams)
525534 else if sym.isLocalToBlock then
526535 if ctx.settings.WunusedHas .locals
527536 && ! sym.isCanEqual
528537 then
529- warnings.addOne( (UnusedSymbol .localDefs, pos) )
538+ warnAt(pos) (UnusedSymbol .localDefs)
530539
531540 if ctx.settings.WunusedHas .patvars then
532541 // convert the one non-synthetic so all are comparable
@@ -536,18 +545,134 @@ object CheckUnused:
536545 val byPos = infos.pats.groupMap(uniformPos(_, _))((sym, pos) => sym)
537546 for (pos, syms) <- byPos if ! syms.exists(_.hasAnnotation(defn.UnusedAnnot )) do
538547 if ! syms.exists(infos.refs(_)) && ! syms.exists(v => ! v.isLocal && ! v.is(Private )) then
539- warnings.addOne( (UnusedSymbol .patVars, pos) )
548+ warnAt(pos) (UnusedSymbol .patVars)
540549
550+ // TODO check for unused masking import
541551 import scala .jdk .CollectionConverters .given
542- val actionable = false && ctx.settings.rewrite.value.nonEmpty
552+ import Rewrites . ActionPatch
543553 if (ctx.settings.WunusedHas .imports || ctx.settings.WunusedHas .strictNoImplicitWarn) && ! infos.isRepl then
544- for imp <- infos.imps.keySet.nn.asScala do
545- if actionable then
546- ???
547- else
548- for sel <- imp.selectors do
549- if ! sel.isImportExclusion && ! infos.sels.containsKey(sel) && ! imp.isLoose(sel) then
550- warnings.addOne((UnusedSymbol .imports(actions = Nil ), sel.srcPos))
554+ type ImpSel = (Import , ImportSelector )
555+ def isUsable (imp : Import , sel : ImportSelector ): Boolean =
556+ sel.isImportExclusion || infos.sels.containsKey(sel) || imp.isLoose(sel)
557+ def warnImport (warnable : ImpSel , actions : List [CodeAction ] = Nil ): Unit =
558+ val (imp, sel) = warnable
559+ val msg = UnusedSymbol .imports(actions)
560+ // example collection.mutable.{Map as MutMap}
561+ val origin = cpy.Import (imp)(imp.expr, List (sel)).show(using ctx.withoutColors).stripPrefix(" import " )
562+ warnAt(sel.srcPos)(msg, origin)
563+
564+ if ! actionable then
565+ for imp <- infos.imps.keySet.nn.asScala; sel <- imp.selectors if ! isUsable(imp, sel) do
566+ warnImport(imp -> sel)
567+ else
568+ // If the rest of the line is blank, include it in the final edit position. (Delete trailing whitespace.)
569+ // If for deletion, and the prefix of the line is also blank, then include that, too. (Del blank line.)
570+ def editPosAt (srcPos : SrcPos , forDeletion : Boolean ): SrcPos =
571+ val start = srcPos.span.start
572+ val end = srcPos.span.end
573+ val content = srcPos.sourcePos.source.content()
574+ val prev = content.lastIndexWhere(c => ! isWhitespace(c), end = start - 1 )
575+ val emptyLeft = prev < 0 || isLineBreakChar(content(prev))
576+ val next = content.indexWhere(c => ! isWhitespace(c), from = end)
577+ val emptyRight = next < 0 || isLineBreakChar(content(next))
578+ val deleteLine = emptyLeft && emptyRight && forDeletion
579+ val bump = if (deleteLine) 1 else 0 // todo improve to include offset of next line, endline + 1
580+ val p0 = srcPos.span
581+ val p1 = if (next >= 0 && emptyRight) p0.withEnd(next + bump) else p0
582+ val p2 = if (deleteLine) p1.withStart(prev + 1 ) else p1
583+ srcPos.sourcePos.withSpan(p2)
584+ def actionsOf (actions : (SrcPos , String )* ): List [CodeAction ] =
585+ val patches = actions.map((srcPos, replacement) => ActionPatch (srcPos.sourcePos, replacement)).toList
586+ List (CodeAction (title = " unused import" , description = Some (" remove import" ), patches))
587+ def replace (editPos : SrcPos )(replacement : String ): List [CodeAction ] = actionsOf(editPos -> replacement)
588+ def deletion (editPos : SrcPos ): List [CodeAction ] = actionsOf(editPos -> " " )
589+ def textFor (impsel : ImpSel ): String =
590+ val (imp, sel) = impsel
591+ val content = imp.srcPos.sourcePos.source.content()
592+ def textAt (pos : SrcPos ) = String (content.slice(pos.span.start, pos.span.end))
593+ val qual = textAt(imp.expr.srcPos) // keep original
594+ val selector = textAt(sel.srcPos) // keep original
595+ s " $qual. $selector" // don't succumb to vagaries of show
596+ // begin actionable
597+ val sortedImps = infos.imps.keySet.nn.asScala.toArray.sorta(_.srcPos.span.point) // sorted by pos
598+ var index = 0
599+ while index < sortedImps.length do
600+ val nextImport = sortedImps.indexSatisfying(from = index + 1 )(_.isPrimaryClause) // next import statement
601+ if sortedImps.indexSatisfying(from = index, until = nextImport): imp =>
602+ imp.selectors.exists(! isUsable(imp, _)) // check if any selector in statement was unused
603+ < nextImport then
604+ // if no usable selectors in the import statement, delete it entirely.
605+ // if there is exactly one usable selector, then replace with just that selector (i.e., format it).
606+ // else for each clause, delete it or format one selector or delete unused selectors.
607+ // To delete a comma separated item, delete start-to-start, but for last item delete a preceding comma.
608+ // Reminder that first clause span includes the keyword, so delete point-to-start instead.
609+ val existing = sortedImps.slice(index, nextImport)
610+ val (keeping, deleting) = existing.iterator.flatMap(imp => imp.selectors.map(imp -> _)).toList
611+ .partition(isUsable(_, _))
612+ if keeping.isEmpty then
613+ val editPos = existing.head.srcPos.sourcePos.withSpan:
614+ Span (start = existing.head.srcPos.span.start, end = existing.last.srcPos.span.end)
615+ deleting.init.foreach(warnImport(_))
616+ warnImport(deleting.last, deletion(editPosAt(editPos, forDeletion = true )))
617+ else if keeping.lengthIs == 1 then
618+ val editPos = existing.head.srcPos.sourcePos.withSpan:
619+ Span (start = existing.head.srcPos.span.start, end = existing.last.srcPos.span.end)
620+ deleting.init.foreach(warnImport(_))
621+ val text = s " import ${textFor(keeping.head)}"
622+ warnImport(deleting.last, replace(editPosAt(editPos, forDeletion = false ))(text))
623+ else
624+ val lostClauses = existing.iterator.filter(imp => ! keeping.exists((i, _) => imp eq i)).toList
625+ for imp <- lostClauses do
626+ val actions =
627+ if imp == existing.last then
628+ val content = imp.srcPos.sourcePos.source.content()
629+ val prev = existing.lastIndexWhere(i0 => keeping.exists((i, _) => i == i0))
630+ val comma = content.indexOf(',' , from = existing(prev).srcPos.span.end)
631+ val commaPos = imp.srcPos.sourcePos.withSpan:
632+ Span (start = comma, end = existing(prev + 1 ).srcPos.span.start)
633+ val srcPos = imp.srcPos
634+ val editPos = srcPos.sourcePos.withSpan: // exclude keyword
635+ srcPos.span.withStart(srcPos.span.point)
636+ actionsOf(commaPos -> " " , editPos -> " " )
637+ else
638+ val impIndex = existing.indexOf(imp)
639+ val editPos = imp.srcPos.sourcePos.withSpan: // exclude keyword
640+ Span (start = imp.srcPos.span.point, end = existing(impIndex + 1 ).srcPos.span.start)
641+ deletion(editPos)
642+ imp.selectors.init.foreach(sel => warnImport(imp -> sel))
643+ warnImport(imp -> imp.selectors.last, actions)
644+ val singletons = existing.iterator.filter(imp => keeping.count((i, _) => imp eq i) == 1 ).toList
645+ var seen = List .empty[Import ]
646+ for impsel <- deleting do
647+ val (imp, sel) = impsel
648+ if singletons.contains(imp) then
649+ if seen.contains(imp) then
650+ warnImport(impsel)
651+ else
652+ seen ::= imp
653+ val editPos = imp.srcPos.sourcePos.withSpan:
654+ Span (start = imp.srcPos.span.point, end = imp.srcPos.span.end) // exclude keyword
655+ val text = textFor(keeping.find((i, _) => imp eq i).get)
656+ warnImport(impsel, replace(editPosAt(editPos, forDeletion = false ))(text))
657+ else if ! lostClauses.contains(imp) then
658+ val actions =
659+ if sel == imp.selectors.last then
660+ val content = sel.srcPos.sourcePos.source.content()
661+ val prev = imp.selectors.lastIndexWhere(s0 => keeping.exists((_, s) => s == s0))
662+ val comma = content.indexOf(',' , from = imp.selectors(prev).srcPos.span.end)
663+ val commaPos = sel.srcPos.sourcePos.withSpan:
664+ Span (start = comma, end = imp.selectors(prev + 1 ).srcPos.span.start)
665+ val editPos = sel.srcPos
666+ actionsOf(commaPos -> " " , editPos -> " " )
667+ else
668+ val selIndex = imp.selectors.indexOf(sel)
669+ val editPos = sel.srcPos.sourcePos.withSpan:
670+ sel.srcPos.span.withEnd(imp.selectors(selIndex + 1 ).srcPos.span.start)
671+ deletion(editPos)
672+ warnImport(impsel, actions)
673+ end if
674+ index = nextImport
675+ end while
551676
552677 warnings.result().sorta(_._2.span.point)
553678 end warnings
@@ -649,18 +774,15 @@ object CheckUnused:
649774 case _ => false
650775
651776 extension (imp : Import )
777+ /** Is it the first import clause in a statement? `a.x` in `import a.x, b,{y, z}` */
778+ def isPrimaryClause (using Context ): Boolean =
779+ val span = imp.srcPos.span
780+ span.start != span.point // primary clause starts at `import` keyword
781+
652782 /** Generated import of cases from enum companion. */
653783 def isGeneratedByEnum (using Context ): Boolean =
654784 imp.symbol.exists && imp.symbol.owner.is(Enum , butNot = Case )
655785
656- /** Checks if import selects a def that is transparent and inline.
657- def isTransparentInline(using Context): Boolean =
658- val qual = imp.expr
659- imp.selectors.exists: sel =>
660- val importedMembers = qual.tpe.member(sel.name).alternatives
661- importedMembers.exists(_.symbol.isAllOf(Transparent | Inline))
662- */
663-
664786 /** Under -Wunused:strict-no-implicit-warn, avoid false positives
665787 * if this selector is a wildcard that might import implicits or
666788 * specifically does import an implicit.
@@ -685,4 +807,10 @@ object CheckUnused:
685807 }
686808 Arrays .sort(arr.asInstanceOf [Array [A | Null ]], cmp)
687809 arr
810+ // returns `until` if not satisfied
811+ def indexSatisfying (from : Int , until : Int = arr.length)(p : A => Boolean ): Int =
812+ var i = from
813+ while i < until && ! p(arr(i)) do
814+ i += 1
815+ i
688816end CheckUnused
0 commit comments