Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@jrudolph
Last active January 17, 2018 13:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jrudolph/0c347fa00a9ebe1f10aca85aac2c4022 to your computer and use it in GitHub Desktop.
Save jrudolph/0c347fa00a9ebe1f10aca85aac2c4022 to your computer and use it in GitHub Desktop.
Cleanup Imports for sbt 1.0
/*
* Copyright (C) 2009-2017 Lightbend Inc. <http://www.lightbend.com>
*/
import sbt._
import Keys._
package sbt {
object Access {
def compilerReporter = sbt.Keys.compilerReporter
}
}
package autoimport {
import java.io.FileWriter
import autoimport.AutoImportFixerKeys.DeleteLine
import sbt.plugins.JvmPlugin
import xsbti.{Severity, Position, Problem}
object AutoImportFixerKeys {
sealed trait Fix
case class DeleteLine(file: File, lineNo: Int, column: Int, lineContent: String) extends Fix
val collectFixes = taskKey[Seq[DeleteLine]]("Collects fixes that could be applied automatically")
val applyFixes = taskKey[Seq[DeleteLine]]("Apply fixes to files")
}
object AutoImportFixer extends AutoPlugin {
val AutoImports = config("auto-imports")
val AutoImportsTest = config("auto-imports-test")
val EndsWithOpenBraceAndOneElement = """(.*)\{\s*([^\s]+)\s*""".r
val ClosingBraceAndWhiteSpace = """\s*\}\s*""".r
class MyReporter(delegate: Option[xsbti.Reporter]) extends xsbti.Reporter {
var allProblems = Vector.empty[Problem]
def hasWarnings: Boolean = delegate.map(_.hasWarnings).getOrElse(false)
def comment(pos: Position, msg: String): Unit = delegate.foreach(_.comment(pos, msg))
def log(p: Problem): Unit =
if (p.severity == Severity.Warn && p.message.startsWith("Unused import")) {
val pos = p.position
println(s"Found unused import at: $pos ${pos.offset().get()} '${pos.lineContent()}'")
allProblems :+= p
} else delegate.foreach(_.log(p))
def problems(): Array[Problem] = delegate.map(_.problems).getOrElse(Array.empty[Problem])
def hasErrors: Boolean = delegate.map(_.hasErrors).getOrElse(false)
def printSummary(): Unit = delegate.foreach(_.printSummary())
def reset(): Unit = {
allProblems = Vector.empty
delegate.foreach(_.reset())
}
}
override def trigger: PluginTrigger = allRequirements
override def requires: Plugins = JvmPlugin
override def projectSettings: Seq[Def.Setting[_]] =
inConfig(Compile)(settings(Compile)) ++ inConfig(Test)(settings(Test)) ++ Seq(
AutoImportFixerKeys.applyFixes := {
(AutoImportFixerKeys.applyFixes in Compile).value ++ (AutoImportFixerKeys.applyFixes in Test).value
}
)
/*forConfig(Compile, Compile, Defaults.compileSettings) ++
forConfig(Test, Test, Defaults.testSettings)*/
def forConfig(config: Configuration, oldConfig: Configuration, defaultSettings: Seq[Setting[_]]) =
inConfig(config)(defaultSettings ++ settings(oldConfig))
def settings(oldConfig: Configuration): Seq[Setting[_]] = Seq(
AutoImportFixerKeys.collectFixes := {
val rep0 = (Access.compilerReporter in compile).value
val _ = compile.value
rep0 match {
case rep: MyReporter =>
println(s"Got ${rep.allProblems.size} problems")
rep.allProblems
.filterNot { p =>
val line = p.position.lineContent
line.contains("language.existentials") // it seems to be a bug in 2.11 that those are needed more often than in 2.12, keep them for now
// ||
//line.contains("import language") ||
//line.endsWith("collection.immutable")
}
.map { p =>
DeleteLine(p.position.sourceFile.get, p.position.line.get, p.position.pointer.get, p.position.lineContent)
}
case _ => Nil
}
},
AutoImportFixerKeys.applyFixes := {
val fixes = AutoImportFixerKeys.collectFixes.value
fixes.groupBy(_.file.getCanonicalPath).foreach {
case (file, fixes) =>
val newFile = java.io.File.createTempFile("out", ".scala")
val out = new FileWriter(newFile)
val lines = fixes.groupBy(_.lineNo)
scala.io.Source.fromFile(file, "utf8").getLines().zipWithIndex.foreach {
case (line, idx) =>
def deleteAt(column: Int): Unit = {
val prefix = line.take(column - 1)
val rest = line.drop(column - 1)
def find(char: Char): Int = {
val res = rest.indexOf(char)
if (res == -1) Int.MaxValue
else res
}
val nextComma = find(',')
val nextClosingBrace = find('}') - 1
require(nextComma < Int.MaxValue || nextClosingBrace < Int.MaxValue)
val end = math.min(nextComma, nextClosingBrace)
val realPrefix: String =
if (end == nextClosingBrace) prefix.reverse.dropWhile(_ != ',').drop(1).reverse
else prefix
val suffix: String = {
val rem = rest.drop(end + 1)
if (rem.startsWith(" ")) rem
else " "+rem
}
// don't leave braces with single element
(realPrefix, suffix) match {
case (EndsWithOpenBraceAndOneElement(textBeforeBrace, oneElement), ClosingBraceAndWhiteSpace()) =>
// matches prefix: "import xyz.abc.{ Blib" suffix: " }" and changes it to "import xyz.abc.Blib"
out.write(textBeforeBrace)
out.write(oneElement)
case _ =>
out.write(realPrefix)
out.write(suffix)
}
out.write('\n')
}
lines.get(idx + 1) match {
case None =>
out.write(line)
out.write('\n')
case Some(Seq(single)) =>
if (single.lineContent contains ",") deleteAt(single.column)
else () // drop line completely
case Some(several) =>
val occs = line.count(_ == ',')
if (occs == several.size - 1) () // delete all
else {
// delete just the first and depend on recompilation to find others
deleteAt(several.head.column)
}
}
}
out.close()
newFile.renameTo(new File(file))
}
fixes
},
// this has the bad side-effect that MyReporter is now responsible for all error reporting
// currently, you will have to enable / disable this line manually to get either compilation
// with normal error reporting or import cleanup functionality
Access.compilerReporter in compile := new MyReporter(Some((Access.compilerReporter in compile).value)),
compileInputs in compile := {
val inputs = (compileInputs in compile in oldConfig).value
inputs.withOptions(inputs.options.withScalacOptions(inputs.options.scalacOptions ++ Seq("-Ywarn-unused-import", "-Ywarn-dead-code", "-Ywarn-unused")))
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment