mirror of https://github.com/sbt/sbt.git
Merge pull request #5153 from eed3si9n/wip/lint
build linting to warn on unused settings during reload
This commit is contained in:
commit
e17c64dfb6
|
|
@ -351,7 +351,7 @@ object Defaults extends BuildCommon {
|
||||||
sys.env.contains("CI") || SysProp.ci,
|
sys.env.contains("CI") || SysProp.ci,
|
||||||
// watch related settings
|
// watch related settings
|
||||||
pollInterval :== Watch.defaultPollInterval,
|
pollInterval :== Watch.defaultPollInterval,
|
||||||
)
|
) ++ LintUnused.lintSettings
|
||||||
)
|
)
|
||||||
|
|
||||||
def defaultTestTasks(key: Scoped): Seq[Setting[_]] =
|
def defaultTestTasks(key: Scoped): Seq[Setting[_]] =
|
||||||
|
|
|
||||||
|
|
@ -492,6 +492,9 @@ object Keys {
|
||||||
private[sbt] val postProgressReports = settingKey[Unit]("Internally used to modify logger.").withRank(DTask)
|
private[sbt] val postProgressReports = settingKey[Unit]("Internally used to modify logger.").withRank(DTask)
|
||||||
@deprecated("No longer used", "1.3.0")
|
@deprecated("No longer used", "1.3.0")
|
||||||
private[sbt] val executeProgress = settingKey[State => TaskProgress]("Experimental task execution listener.").withRank(DTask)
|
private[sbt] val executeProgress = settingKey[State => TaskProgress]("Experimental task execution listener.").withRank(DTask)
|
||||||
|
val lintUnused = inputKey[Unit]("Check for keys unused by other settings and tasks.")
|
||||||
|
val excludeLintKeys = settingKey[Set[Def.KeyedInitialize[_]]]("Keys excluded from lintUnused task")
|
||||||
|
val includeLintKeys = settingKey[Set[Def.KeyedInitialize[_]]]("Task keys that are included into lintUnused task")
|
||||||
|
|
||||||
val stateStreams = AttributeKey[Streams]("stateStreams", "Streams manager, which provides streams for different contexts. Setting this on State will override the default Streams implementation.")
|
val stateStreams = AttributeKey[Streams]("stateStreams", "Streams manager, which provides streams for different contexts. Setting this on State will override the default Streams implementation.")
|
||||||
val resolvedScoped = Def.resolvedScoped
|
val resolvedScoped = Def.resolvedScoped
|
||||||
|
|
|
||||||
|
|
@ -833,10 +833,10 @@ object BuiltinCommands {
|
||||||
checkSBTVersionChanged(s0)
|
checkSBTVersionChanged(s0)
|
||||||
val (s1, base) = Project.loadAction(SessionVar.clear(s0), action)
|
val (s1, base) = Project.loadAction(SessionVar.clear(s0), action)
|
||||||
IO.createDirectory(base)
|
IO.createDirectory(base)
|
||||||
val s = if (s1 has Keys.stateCompilerCache) s1 else registerCompilerCache(s1)
|
val s2 = if (s1 has Keys.stateCompilerCache) s1 else registerCompilerCache(s1)
|
||||||
|
|
||||||
val (eval, structure) =
|
val (eval, structure) =
|
||||||
try Load.defaultLoad(s, base, s.log, Project.inPluginProject(s), Project.extraBuilds(s))
|
try Load.defaultLoad(s2, base, s2.log, Project.inPluginProject(s2), Project.extraBuilds(s2))
|
||||||
catch {
|
catch {
|
||||||
case ex: compiler.EvalException =>
|
case ex: compiler.EvalException =>
|
||||||
s0.log.debug(ex.getMessage)
|
s0.log.debug(ex.getMessage)
|
||||||
|
|
@ -846,12 +846,13 @@ object BuiltinCommands {
|
||||||
}
|
}
|
||||||
|
|
||||||
val session = Load.initialSession(structure, eval, s0)
|
val session = Load.initialSession(structure, eval, s0)
|
||||||
SessionSettings.checkSession(session, s)
|
SessionSettings.checkSession(session, s2)
|
||||||
addCacheStoreFactoryFactory(
|
val s3 = addCacheStoreFactoryFactory(
|
||||||
Project
|
Project
|
||||||
.setProject(session, structure, s)
|
.setProject(session, structure, s2)
|
||||||
.put(sbt.nio.Keys.hasCheckedMetaBuild, new AtomicBoolean(false))
|
.put(sbt.nio.Keys.hasCheckedMetaBuild, new AtomicBoolean(false))
|
||||||
)
|
)
|
||||||
|
LintUnused.lintUnusedFunc(s3)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val addCacheStoreFactoryFactory: State => State = (s: State) => {
|
private val addCacheStoreFactoryFactory: State => State = (s: State) => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
/*
|
||||||
|
* sbt
|
||||||
|
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||||
|
* Copyright 2008 - 2010, Mark Harrah
|
||||||
|
* Licensed under Apache License 2.0 (see LICENSE)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sbt
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import Keys._
|
||||||
|
import Def.{ Setting, ScopedKey }
|
||||||
|
import sbt.internal.util.{ FilePosition, NoPosition, SourcePosition }
|
||||||
|
import java.io.File
|
||||||
|
import Scope.Global
|
||||||
|
import sbt.Def._
|
||||||
|
|
||||||
|
object LintUnused {
|
||||||
|
lazy val lintSettings: Seq[Setting[_]] = Seq(
|
||||||
|
excludeLintKeys := Set(
|
||||||
|
aggregate,
|
||||||
|
concurrentRestrictions,
|
||||||
|
commands,
|
||||||
|
crossScalaVersions,
|
||||||
|
onLoadMessage,
|
||||||
|
sbt.nio.Keys.watchTriggers,
|
||||||
|
),
|
||||||
|
includeLintKeys := Set(
|
||||||
|
scalacOptions,
|
||||||
|
javacOptions,
|
||||||
|
javaOptions,
|
||||||
|
incOptions,
|
||||||
|
compileOptions,
|
||||||
|
packageOptions,
|
||||||
|
mainClass,
|
||||||
|
mappings,
|
||||||
|
testOptions,
|
||||||
|
classpathConfiguration,
|
||||||
|
ivyConfiguration,
|
||||||
|
),
|
||||||
|
Keys.lintUnused := lintUnusedTask.evaluated,
|
||||||
|
)
|
||||||
|
|
||||||
|
// input task version of the lintUnused
|
||||||
|
def lintUnusedTask: Def.Initialize[InputTask[Unit]] = Def.inputTask {
|
||||||
|
val _ = Def.spaceDelimited().parsed // not used yet
|
||||||
|
val state = Keys.state.value
|
||||||
|
val log = streams.value.log
|
||||||
|
val includeKeys = (includeLintKeys in Global).value map { _.scopedKey.key.label }
|
||||||
|
val excludeKeys = (excludeLintKeys in Global).value map { _.scopedKey.key.label }
|
||||||
|
val result = lintUnused(state, includeKeys, excludeKeys)
|
||||||
|
if (result.isEmpty) log.success("ok")
|
||||||
|
else lintResultLines(result) foreach { log.warn(_) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// function version of the lintUnused, based on just state
|
||||||
|
def lintUnusedFunc(s: State): State = {
|
||||||
|
val log = s.log
|
||||||
|
val extracted = Project.extract(s)
|
||||||
|
val includeKeys = extracted.get(includeLintKeys in Global) map { _.scopedKey.key.label }
|
||||||
|
val excludeKeys = extracted.get(excludeLintKeys in Global) map { _.scopedKey.key.label }
|
||||||
|
val result = lintUnused(s, includeKeys, excludeKeys)
|
||||||
|
lintResultLines(result) foreach { log.warn(_) }
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
def lintResultLines(
|
||||||
|
result: Seq[(ScopedKey[_], String, Vector[SourcePosition])]
|
||||||
|
): Vector[String] = {
|
||||||
|
import scala.collection.mutable.ListBuffer
|
||||||
|
val buffer = ListBuffer.empty[String]
|
||||||
|
|
||||||
|
val size = result.size
|
||||||
|
if (size == 1) buffer.append("there's a key that's not used by any other settings/tasks:")
|
||||||
|
else buffer.append(s"there are $size keys that are not used by any other settings/tasks:")
|
||||||
|
buffer.append(" ")
|
||||||
|
result foreach {
|
||||||
|
case (_, str, positions) =>
|
||||||
|
buffer.append(s"* $str")
|
||||||
|
positions foreach {
|
||||||
|
case pos: FilePosition => buffer.append(s" +- ${pos.path}:${pos.startLine}")
|
||||||
|
case _ => ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer.append(" ")
|
||||||
|
buffer.append(
|
||||||
|
"note: a setting might still be used by a command; to exclude a key from this `lintUnused` check"
|
||||||
|
)
|
||||||
|
buffer.append(
|
||||||
|
"either append it to `Global / excludeLintKeys` or call .withRank(KeyRanks.Invisible) on the key"
|
||||||
|
)
|
||||||
|
buffer.toVector
|
||||||
|
}
|
||||||
|
|
||||||
|
def lintUnused(
|
||||||
|
state: State,
|
||||||
|
includeKeys: Set[String],
|
||||||
|
excludeKeys: Set[String]
|
||||||
|
): Seq[(ScopedKey[_], String, Vector[SourcePosition])] = {
|
||||||
|
val extracted = Project.extract(state)
|
||||||
|
val structure = extracted.structure
|
||||||
|
val display = Def.showShortKey(None) // extracted.showKey
|
||||||
|
val actual = true
|
||||||
|
val comp =
|
||||||
|
Def.compiled(structure.settings, actual)(structure.delegates, structure.scopeLocal, display)
|
||||||
|
val cMap = Def.flattenLocals(comp)
|
||||||
|
val used: Set[ScopedKey[_]] = cMap.values.flatMap(_.dependencies).toSet
|
||||||
|
val unused: Seq[ScopedKey[_]] = cMap.keys.filter(!used.contains(_)).toSeq
|
||||||
|
val withDefinedAts: Seq[UnusedKey] = unused map { u =>
|
||||||
|
val definingScope = structure.data.definingScope(u.scope, u.key)
|
||||||
|
val definingScoped = definingScope match {
|
||||||
|
case Some(sc) => ScopedKey(sc, u.key)
|
||||||
|
case _ => u
|
||||||
|
}
|
||||||
|
val definedAt = comp.get(definingScoped) match {
|
||||||
|
case Some(c) => definedAtString(c.settings.toVector)
|
||||||
|
case _ => Vector.empty
|
||||||
|
}
|
||||||
|
val data = Project.scopedKeyData(structure, u.scope, u.key)
|
||||||
|
UnusedKey(u, definedAt, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
def isIncludeKey(u: UnusedKey): Boolean = includeKeys(u.scoped.key.label)
|
||||||
|
def isExcludeKey(u: UnusedKey): Boolean = excludeKeys(u.scoped.key.label)
|
||||||
|
def isSettingKey(u: UnusedKey): Boolean = u.data match {
|
||||||
|
case Some(data) => data.settingValue.isDefined
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
def isLocallyDefined(u: UnusedKey): Boolean = u.positions exists {
|
||||||
|
case pos: FilePosition => pos.path.contains(File.separator)
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
def isInvisible(u: UnusedKey): Boolean = u.scoped.key.rank == KeyRanks.Invisible
|
||||||
|
val unusedKeys = withDefinedAts collect {
|
||||||
|
case u
|
||||||
|
if !isExcludeKey(u) && !isInvisible(u)
|
||||||
|
&& (isSettingKey(u) || isIncludeKey(u))
|
||||||
|
&& isLocallyDefined(u) =>
|
||||||
|
u
|
||||||
|
}
|
||||||
|
(unusedKeys map { u =>
|
||||||
|
(u.scoped, display.show(u.scoped), u.positions)
|
||||||
|
}).sortBy(_._2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private[this] case class UnusedKey(
|
||||||
|
scoped: ScopedKey[_],
|
||||||
|
positions: Vector[SourcePosition],
|
||||||
|
data: Option[ScopedKeyData[_]]
|
||||||
|
)
|
||||||
|
|
||||||
|
private def definedAtString(settings: Vector[Setting[_]]): Vector[SourcePosition] = {
|
||||||
|
settings flatMap { setting =>
|
||||||
|
setting.pos match {
|
||||||
|
case NoPosition => Vector.empty
|
||||||
|
case pos => Vector(pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
ThisBuild / doc / scalacOptions += "-Xsomething"
|
||||||
|
|
||||||
|
lazy val lintBuildTest = taskKey[Unit]("")
|
||||||
|
|
||||||
|
lazy val root = (project in file("."))
|
||||||
|
.settings(
|
||||||
|
lintBuildTest := {
|
||||||
|
val state = Keys.state.value
|
||||||
|
val includeKeys = (includeLintKeys in Global).value map { _.scopedKey.key.label }
|
||||||
|
val excludeKeys = (excludeLintKeys in Global).value map { _.scopedKey.key.label }
|
||||||
|
val result = sbt.internal.LintUnused.lintUnused(state, includeKeys, excludeKeys)
|
||||||
|
assert(result.size == 1)
|
||||||
|
assert(result(0)._2 == "ThisBuild / doc / scalacOptions", result(0)._2)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val app = project
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
> lintBuildTest
|
||||||
Loading…
Reference in New Issue