Skip to content

Commit 92b5d74

Browse files
committed
Rewrite imports, supply origin
1 parent 4e9f747 commit 92b5d74

File tree

7 files changed

+305
-60
lines changed

7 files changed

+305
-60
lines changed

compiler/src/dotty/tools/dotc/report.scala

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
package dotty.tools.dotc
22

3-
import reporting.*
4-
import Diagnostic.*
5-
import util.{SourcePosition, NoSourcePosition, SrcPos}
6-
import core.*
7-
import Contexts.*, Flags.*, Symbols.*, Decorators.*
8-
import config.SourceVersion
93
import ast.*
10-
import config.Feature.sourceVersion
4+
import core.*, Contexts.*, Flags.*, Symbols.*, Decorators.*
5+
import config.Feature.sourceVersion, config.{MigrationVersion, SourceVersion}
6+
import reporting.*, Diagnostic.*
7+
import util.{SourcePosition, NoSourcePosition, SrcPos}
8+
119
import java.lang.System.currentTimeMillis
12-
import dotty.tools.dotc.config.MigrationVersion
1310

1411
object report:
1512

@@ -55,6 +52,9 @@ object report:
5552
else issueWarning(new FeatureWarning(msg, pos.sourcePos))
5653
end featureWarning
5754

55+
def warning(msg: Message, pos: SrcPos, origin: String)(using Context): Unit =
56+
issueWarning(LintWarning(msg, addInlineds(pos), origin))
57+
5858
def warning(msg: Message, pos: SrcPos)(using Context): Unit =
5959
issueWarning(new Warning(msg, addInlineds(pos)))
6060

compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ object Diagnostic:
3636
pos: SourcePosition
3737
) extends Error(msg, pos)
3838

39+
/** A Warning with an origin. The semantics of `origin` depend on the warning.
40+
* For example, an unused import warning has an origin that specifies the unused selector.
41+
* The origin of a deprecation is the deprecated element.
42+
*/
43+
trait OriginWarning(val origin: String):
44+
self: Warning =>
45+
46+
/** Lints are likely to be filtered. Provide extra axes for filtering by `-Wconf`.
47+
*/
48+
class LintWarning(msg: Message, pos: SourcePosition, origin: String)
49+
extends Warning(msg, pos), OriginWarning(origin)
50+
3951
class Warning(
4052
msg: Message,
4153
pos: SourcePosition
@@ -73,13 +85,9 @@ object Diagnostic:
7385
def enablingOption(using Context): Setting[Boolean] = ctx.settings.unchecked
7486
}
7587

76-
class DeprecationWarning(
77-
msg: Message,
78-
pos: SourcePosition,
79-
val origin: String
80-
) extends ConditionalWarning(msg, pos) {
88+
class DeprecationWarning(msg: Message, pos: SourcePosition, origin: String)
89+
extends ConditionalWarning(msg, pos), OriginWarning(origin):
8190
def enablingOption(using Context): Setting[Boolean] = ctx.settings.deprecation
82-
}
8391

8492
class MigrationWarning(
8593
msg: Message,

compiler/src/dotty/tools/dotc/reporting/WConf.scala

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import scala.annotation.internal.sharable
1414
import scala.util.matching.Regex
1515

1616
enum MessageFilter:
17-
def matches(message: Diagnostic): Boolean = this match
17+
def matches(message: Diagnostic): Boolean =
18+
import Diagnostic.*
19+
this match
1820
case Any => true
19-
case Deprecated => message.isInstanceOf[Diagnostic.DeprecationWarning]
20-
case Feature => message.isInstanceOf[Diagnostic.FeatureWarning]
21-
case Unchecked => message.isInstanceOf[Diagnostic.UncheckedWarning]
21+
case Deprecated => message.isInstanceOf[DeprecationWarning]
22+
case Feature => message.isInstanceOf[FeatureWarning]
23+
case Unchecked => message.isInstanceOf[UncheckedWarning]
2224
case MessageID(errorId) => message.msg.errorId == errorId
2325
case MessagePattern(pattern) =>
2426
val noHighlight = message.msg.message.replaceAll("\\e\\[[\\d;]*[^\\d;]","")
@@ -31,7 +33,7 @@ enum MessageFilter:
3133
pattern.findFirstIn(path).nonEmpty
3234
case Origin(pattern) =>
3335
message match
34-
case message: Diagnostic.DeprecationWarning => pattern.findFirstIn(message.origin).nonEmpty
36+
case message: OriginWarning => pattern.findFirstIn(message.origin).nonEmpty
3537
case _ => false
3638
case None => false
3739

@@ -56,12 +58,12 @@ object WConf:
5658
private type Conf = (List[MessageFilter], Action)
5759

5860
def parseAction(s: String): Either[List[String], Action] = s match
59-
case "error" | "e" => Right(Error)
60-
case "warning" | "w" => Right(Warning)
61-
case "verbose" | "v" => Right(Verbose)
62-
case "info" | "i" => Right(Info)
63-
case "silent" | "s" => Right(Silent)
64-
case _ => Left(List(s"unknown action: `$s`"))
61+
case "error" | "e" => Right(Error)
62+
case "warning" | "w" => Right(Warning)
63+
case "verbose" | "v" => Right(Verbose)
64+
case "info" | "i" => Right(Info)
65+
case "silent" | "s" => Right(Silent)
66+
case _ => Left(List(s"unknown action: `$s`"))
6567

6668
private def regex(s: String) =
6769
try Right(s.r)

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

Lines changed: 160 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import dotty.tools.dotc.core.StdNames.nme
1313
import dotty.tools.dotc.core.Symbols.{ClassSymbol, NoSymbol, Symbol, defn, isDeprecated, requiredClass, requiredModule}
1414
import dotty.tools.dotc.core.Types.*
1515
import 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
1718
import dotty.tools.dotc.transform.MegaPhase.MiniPhase
1819
import 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}
2022
import dotty.tools.dotc.util.chaining.*
2123

2224
import 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
688816
end CheckUnused

0 commit comments

Comments
 (0)