Skip to content

Commit a79d0ca

Browse files
committed
Clean up rewrite unused imports
1 parent 9fb4a7d commit a79d0ca

File tree

5 files changed

+175
-46
lines changed

5 files changed

+175
-46
lines changed

compiler/src/dotty/tools/dotc/core/Contexts.scala

+10-45
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ import reporting._
2525
import io.{AbstractFile, NoAbstractFile, PlainFile, Path}
2626
import scala.io.Codec
2727
import collection.mutable
28-
import parsing.Parsers
2928
import printing._
30-
import config.Printers.{implicits, implicitsDetailed}
3129
import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings, ScalaRelease}
3230
import classfile.ReusableDataReader
3331
import StdNames.nme
@@ -1027,7 +1025,6 @@ object Contexts:
10271025
/** Collect information about the run for purposes of additional diagnostics.
10281026
*/
10291027
class Usages:
1030-
import rewrites.Rewrites.patch
10311028
private val selectors = mutable.Map.empty[ImportInfo, Set[untpd.ImportSelector]].withDefaultValue(Set.empty)
10321029
private val importInfos = mutable.Map.empty[CompilationUnit, List[(ImportInfo, Symbol)]].withDefaultValue(Nil)
10331030

@@ -1043,60 +1040,28 @@ object Contexts:
10431040
selectors(info) += selector
10441041

10451042
// unused import, owner, which selector
1046-
def unused(using Context): List[(ImportInfo, Symbol, untpd.ImportSelector)] =
1047-
var unusages = List.empty[(ImportInfo, Symbol, untpd.ImportSelector)]
1043+
def unused(using Context): List[(ImportInfo, Symbol, List[untpd.ImportSelector])] =
10481044
if ctx.settings.WunusedHas.imports && !ctx.compilationUnit.isJava then
1049-
//if ctx.settings.Ydebug.value then
1050-
// println(importInfos.get(ctx.compilationUnit).map(iss => iss.map((ii, s) => s"${ii.show} ($ii)")).getOrElse(Nil).mkString("Registered ImportInfos\n", "\n", ""))
1051-
// println(selectors.toList.flatMap((k,v) => v.toList.map(sel => s"${k.show} -> $sel")).mkString("Used selectors\n", "\n", ""))
1045+
var unusages = List.empty[(ImportInfo, Symbol, List[untpd.ImportSelector])]
10521046
def checkUsed(info: ImportInfo, owner: Symbol): Unit =
1053-
val used = selectors(info)
1054-
var needsPatch = false
1047+
val usedSelectors = selectors(info)
1048+
var unusedSelectors = List.empty[untpd.ImportSelector]
10551049
def cull(toCheck: List[untpd.ImportSelector]): Unit =
10561050
toCheck match
10571051
case selector :: rest =>
10581052
cull(rest) // reverse
1059-
if !selector.isMask && !used(selector) then
1060-
unusages ::= ((info, owner, selector))
1061-
needsPatch = true
1053+
if !selector.isMask && !usedSelectors(selector) then
1054+
unusedSelectors ::= selector
10621055
case _ =>
10631056
cull(info.selectors)
1064-
if needsPatch && ctx.settings.YrewriteImports.value then
1065-
val src = ctx.compilationUnit.source
1066-
val infoPos = info.qualifier.sourcePos
1067-
val lineSource = SourceFile.virtual(name = "import-line.scala", content = infoPos.lineContent)
1068-
val PackageDef(_, pieces) = Parsers.Parser(lineSource).parse(): @unchecked
1069-
// patch if there's just one import on the line, i.e., not import a.b, c.d
1070-
if pieces.length == 1 then
1071-
val retained = info.selectors.filter(sel => sel.isMask || used(sel))
1072-
val selectorSpan = info.selectors.map(_.span).reduce(_ union _)
1073-
val lineSpan = src.lineSpan(infoPos.start)
1074-
if retained.isEmpty then
1075-
patch(src, lineSpan, "") // line deletion
1076-
else if retained.size == 1 && info.selectors.size > 1 then
1077-
var starting = info.selectors.head.span.start
1078-
while starting > lineSpan.start && src.content()(starting) != '{' do starting -= 1
1079-
var ending = info.selectors.last.span.end
1080-
while ending <= lineSpan.end && src.content()(ending) != '}' do ending += 1
1081-
if ending < lineSpan.end then ending += 1 // past the close brace
1082-
val widened = selectorSpan.withStart(starting).withEnd(ending)
1083-
patch(src, widened, toText(retained)) // try to remove braces
1084-
else
1085-
patch(src, selectorSpan, toText(retained))
1057+
if unusedSelectors.nonEmpty then unusages ::= (info, owner, unusedSelectors)
10861058
end checkUsed
10871059
importInfos.remove(ctx.compilationUnit).foreach(_.foreach(checkUsed))
1088-
unusages
1060+
unusages
1061+
else
1062+
Nil
10891063
end unused
10901064

1091-
// just the selectors, no need to add braces
1092-
private def toText(retained: List[untpd.ImportSelector])(using Context): String =
1093-
def selected(sel: untpd.ImportSelector) =
1094-
if sel.isGiven then "given"
1095-
else if sel.isWildcard then "*"
1096-
else if sel.name == sel.rename then sel.name.show
1097-
else s"${sel.name.show} as ${sel.rename.show}"
1098-
retained.map(selected).mkString(", ")
1099-
11001065
def clear()(using Context): Unit =
11011066
importInfos.clear()
11021067
selectors.clear()

compiler/src/dotty/tools/dotc/typer/TyperPhase.scala

+42-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dotty.tools
22
package dotc
33
package typer
44

5+
import ast.untpd
56
import core._
67
import Phases._
78
import Contexts._
@@ -56,7 +57,47 @@ class TyperPhase(addRootImports: Boolean = true) extends Phase {
5657
}
5758

5859
def emitDiagnostics(using Context): Unit =
59-
ctx.usages.unused.foreach((importInfo, owner, selector) => report.warning(s"Unused import", pos = selector.srcPos))
60+
ctx.usages.unused.foreach { (info, owner, selectors) =>
61+
import rewrites.Rewrites.patch
62+
import parsing.Parsers
63+
import util.SourceFile
64+
import ast.Trees.*
65+
def reportSelectors() = selectors.foreach(selector => report.warning(s"Unused import", pos = selector.srcPos))
66+
if ctx.settings.YrewriteImports.value then
67+
val src = ctx.compilationUnit.source
68+
val infoPos = info.qualifier.sourcePos
69+
val lineSource = SourceFile.virtual(name = "import-line.scala", content = infoPos.lineContent)
70+
val PackageDef(_, pieces) = Parsers.Parser(lineSource).parse(): @unchecked
71+
// patch if there's just one import on the line, i.e., not import a.b, c.d
72+
if pieces.length == 1 then
73+
val retained = info.selectors.filterNot(selectors.contains)
74+
val selectorSpan = info.selectors.map(_.span).reduce(_ union _)
75+
val lineSpan = src.lineSpan(infoPos.start)
76+
if retained.isEmpty then
77+
patch(src, lineSpan, "") // line deletion
78+
else if retained.size == 1 && info.selectors.size > 1 then
79+
var starting = info.selectors.head.span.start
80+
while starting > lineSpan.start && src.content()(starting) != '{' do starting -= 1
81+
var ending = info.selectors.last.span.end
82+
while ending <= lineSpan.end && src.content()(ending) != '}' do ending += 1
83+
if ending < lineSpan.end then ending += 1 // past the close brace
84+
val widened = selectorSpan.withStart(starting).withEnd(ending)
85+
patch(src, widened, toText(retained)) // try to remove braces
86+
else
87+
patch(src, selectorSpan, toText(retained))
88+
else
89+
reportSelectors()
90+
else
91+
reportSelectors()
92+
}
93+
// just the selectors, no need to add braces
94+
private def toText(retained: List[untpd.ImportSelector])(using Context): String =
95+
def selected(sel: untpd.ImportSelector) =
96+
if sel.isGiven then "given"
97+
else if sel.isWildcard then "*"
98+
else if sel.name == sel.rename then sel.name.show
99+
else s"${sel.name.show} as ${sel.rename.show}"
100+
retained.map(selected).mkString(", ")
60101
def clearDiagnostics()(using Context): Unit =
61102
ctx.usages.clear()
62103

compiler/test/dotty/tools/dotc/CompilationTests.scala

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class CompilationTests {
7979
compileFile("tests/rewrites/i9632.scala", defaultOptions.and("-indent", "-rewrite")),
8080
compileFile("tests/rewrites/i11895.scala", defaultOptions.and("-indent", "-rewrite")),
8181
compileFile("tests/rewrites/i12340.scala", unindentOptions.and("-rewrite")),
82+
compileFile("tests/rewrites/unused-imports.scala", defaultOptions),
8283
).checkRewrites()
8384
}
8485

tests/rewrites/unused-imports.check

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// scalac: -Wunused:imports -Werror -feature -rewrite -Yrewrite-imports
2+
3+
import language.implicitConversions
4+
import scala.concurrent.ExecutionContext.Implicits.*
5+
6+
class C:
7+
def c = 42
8+
import scala.collection.mutable.{Seq as _, ListBuffer, Set as OK} //?error //?error
9+
10+
def buf = ListBuffer.empty[String]
11+
def ok: OK[Int] = ???
12+
def f = concurrent.Future(42) // implicit usage
13+
14+
import Thread.*
15+
import State.{NEW, BLOCKED}
16+
17+
def state(t: Thread) =
18+
t.getState match
19+
case NEW =>
20+
import State.RUNNABLE
21+
t.getState match
22+
case RUNNABLE => 0
23+
case BLOCKED => 1
24+
case _ => -1
25+
case _ => -1
26+
27+
enum E:
28+
case E0, E1
29+
30+
def e(x: E) =
31+
x match
32+
case E.E0 => "e0"
33+
case E.E1 => "e1"
34+
35+
locally {
36+
import Givens.{*, given}
37+
38+
def g(s: String)(using Ordering[C]) = ???
39+
def ordered = g(greeting)
40+
}
41+
locally {
42+
import Givens.{cOrdering, *}
43+
44+
def g(s: String)(using Ordering[C]) = ???
45+
def ordered = g(greeting)
46+
}
47+
48+
object Givens:
49+
given cOrdering: Ordering[C] with
50+
override def compare(c0: C, c1: C) = 0
51+
val greeting = "we love Givens"
52+
53+
class A:
54+
def f(b: B): Unit = b.f(this)
55+
56+
object A:
57+
implicit val a2b: Conversion[A, B] = _ => B()
58+
59+
class B:
60+
def f(b: B): Unit = ()

tests/rewrites/unused-imports.scala

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// scalac: -Wunused:imports -Werror -feature -rewrite -Yrewrite-imports
2+
3+
import language.implicitConversions
4+
import language.postfixOps //?error
5+
import scala.concurrent.* //?error
6+
import scala.concurrent.ExecutionContext.Implicits.*
7+
8+
class C:
9+
def c = 42
10+
import scala.collection.mutable.{HashMap as GoodMap, Seq as _, ListBuffer, Buffer, Set as OK} //?error //?error
11+
12+
def buf = ListBuffer.empty[String]
13+
def ok: OK[Int] = ???
14+
def f = concurrent.Future(42) // implicit usage
15+
16+
import Thread.*
17+
import State.{NEW, BLOCKED}
18+
19+
def state(t: Thread) =
20+
t.getState match
21+
case NEW =>
22+
import State.RUNNABLE
23+
t.getState match
24+
case RUNNABLE => 0
25+
case BLOCKED => 1
26+
case _ => -1
27+
case _ => -1
28+
29+
enum E:
30+
case E0, E1
31+
32+
def e(x: E) =
33+
x match
34+
case E.E0 => "e0"
35+
case E.E1 => "e1"
36+
37+
locally {
38+
import Givens.{*, given}
39+
40+
def g(s: String)(using Ordering[C]) = ???
41+
def ordered = g(greeting)
42+
}
43+
locally {
44+
import Givens.{cOrdering, *}
45+
46+
def g(s: String)(using Ordering[C]) = ???
47+
def ordered = g(greeting)
48+
}
49+
50+
object Givens:
51+
given cOrdering: Ordering[C] with
52+
override def compare(c0: C, c1: C) = 0
53+
val greeting = "we love Givens"
54+
55+
class A:
56+
def f(b: B): Unit = b.f(this)
57+
58+
object A:
59+
implicit val a2b: Conversion[A, B] = _ => B()
60+
61+
class B:
62+
def f(b: B): Unit = ()

0 commit comments

Comments
 (0)