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,
|
||||
// watch related settings
|
||||
pollInterval :== Watch.defaultPollInterval,
|
||||
)
|
||||
) ++ LintUnused.lintSettings
|
||||
)
|
||||
|
||||
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)
|
||||
@deprecated("No longer used", "1.3.0")
|
||||
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 resolvedScoped = Def.resolvedScoped
|
||||
|
|
|
|||
|
|
@ -833,10 +833,10 @@ object BuiltinCommands {
|
|||
checkSBTVersionChanged(s0)
|
||||
val (s1, base) = Project.loadAction(SessionVar.clear(s0), action)
|
||||
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) =
|
||||
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 {
|
||||
case ex: compiler.EvalException =>
|
||||
s0.log.debug(ex.getMessage)
|
||||
|
|
@ -846,12 +846,13 @@ object BuiltinCommands {
|
|||
}
|
||||
|
||||
val session = Load.initialSession(structure, eval, s0)
|
||||
SessionSettings.checkSession(session, s)
|
||||
addCacheStoreFactoryFactory(
|
||||
SessionSettings.checkSession(session, s2)
|
||||
val s3 = addCacheStoreFactoryFactory(
|
||||
Project
|
||||
.setProject(session, structure, s)
|
||||
.setProject(session, structure, s2)
|
||||
.put(sbt.nio.Keys.hasCheckedMetaBuild, new AtomicBoolean(false))
|
||||
)
|
||||
LintUnused.lintUnusedFunc(s3)
|
||||
}
|
||||
|
||||
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