Skip to content

Instantly share code, notes, and snippets.

@jrudolph
Created February 23, 2016 13:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jrudolph/432298193b071604b997 to your computer and use it in GitHub Desktop.
Save jrudolph/432298193b071604b997 to your computer and use it in GitHub Desktop.
Skeleton for a cleanup unused imports plugin
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")
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(pos: Position, msg: String, sev: Severity): Unit =
if (sev == Severity.Warn && msg.startsWith("Unused import")) {
println(s"Found unused import at: $pos ${pos.offset().get()} '${pos.lineContent()}'")
allProblems :+= Logger.problem("", pos, msg, sev)
} else delegate.foreach(_.log(pos, msg, sev))
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, AutoImportFixerKeys.applyFixes in Test).map((a, b) => a ++ b)
)
/*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 <<= (Access.compilerReporter in compile, compile).map { (rep0, _) =>
rep0 match {
case Some(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 <<= AutoImportFixerKeys.collectFixes map { fixes =>
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 =
if (end == nextClosingBrace) prefix.reverse.dropWhile(_ != ',').drop(1).reverse
else prefix
val suffix = rest.drop(end + 1)
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 ~= (old => Some(new MyReporter(old))),
compileInputs in compile <<= compileInputs in compile in oldConfig map { inputs =>
inputs.copy(config = inputs.config.copy(options = inputs.config.options ++ 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