Fix #5647: treat keys consumed via .all(ScopeFilter) as used by lintUnused

- Add Initialize.dynamicDependencies (Seq[Any]) in util-collection to carry
  (AttributeKey, ScopeFilter) from .all() without changing static dependency graph.
- Add DynamicDepsInitialize wrapper and Def.withDynamicDependencies; use them
  in SettingKeyAll.all and TaskKeyAll.all when the init is a KeyedInitialize.
- In LintUnused, collect init.dynamicDependencies from all settings, expand
  via ScopeFilter.expandDynamicDeps (build ScopeFilter.Data from structure,
  apply each filter to get scopes, add ScopedKey(scope, key) to used).
- Add ScopeFilter.dataFromStructure and expandDynamicDeps with try/catch
  so bad filters or structure do not break the lint.
This commit is contained in:
bitloi 2026-02-09 15:55:12 +01:00
parent edd7061f15
commit 905862f60c
3 changed files with 88 additions and 7 deletions

View File

@ -8,7 +8,7 @@
package sbt
import sbt.internal.{ Load, LoadedBuildUnit }
import sbt.internal.{ BuildStructure, Load, LoadedBuildUnit }
import sbt.internal.util.{ AttributeKey, Dag }
import sbt.librarymanagement.{ ConfigRef, Configuration }
import sbt.internal.util.Types.const
@ -96,8 +96,14 @@ object ScopeFilter {
* Evaluates the initialization in all scopes selected by the filter. These are dynamic dependencies, so
* static inspections will not show them.
*/
def all(sfilter: => ScopeFilter): Initialize[Seq[A]] = Def.flatMap(getData) { data =>
sfilter(data).toSeq.map(s => Project.inScope(s, i)).join
def all(sfilter: => ScopeFilter): Initialize[Seq[A]] = {
val inner = Def.flatMap(getData) { data =>
sfilter(data).toSeq.map(s => Project.inScope(s, i)).join
}
val dynamicDeps = i match
case k: Def.KeyedInitialize[?] => Seq((k.scopedKey.key, sfilter))
case _ => Nil
Def.withDynamicDependencies(inner, dynamicDeps)
}
final class TaskKeyAll[A] private[sbt] (i: Initialize[Task[A]]):
@ -105,9 +111,15 @@ object ScopeFilter {
* Evaluates the task in all scopes selected by the filter. These are dynamic dependencies, so
* static inspections will not show them.
*/
def all(sfilter: => ScopeFilter): Initialize[Task[Seq[A]]] = Def.flatMap(getData) { data =>
import std.TaskExtra.*
sfilter(data).toSeq.map(s => Project.inScope(s, i)).join(_.join)
def all(sfilter: => ScopeFilter): Initialize[Task[Seq[A]]] = {
val inner = Def.flatMap(getData) { data =>
import std.TaskExtra.*
sfilter(data).toSeq.map(s => Project.inScope(s, i)).join(_.join)
}
val dynamicDeps = i match
case k: Def.KeyedInitialize[?] => Seq((k.scopedKey.key, sfilter))
case _ => Nil
Def.withDynamicDependencies(inner, dynamicDeps)
}
private[sbt] val Make = new Make {}
@ -218,6 +230,41 @@ object ScopeFilter {
val allScopes: AllScopes
)
private def dataFromStructure(structure: BuildStructure): Data = {
val units = structure.units
val rootProject = Load.getRootProject(units)
val resolve: ProjectReference => ProjectRef = ref =>
Scope.resolveProjectRef(structure.root, rootProject, ref)
val scopes = structure.data.scopes
val grouped: ScopeMap = scopes
.groupBy(_.project)
.view
.mapValues { byProj =>
byProj.groupBy(_.config).view.mapValues { byConfig =>
byConfig.groupBy(_.task).view.mapValues(_.toSet).toMap
}.toMap
}
.toMap
new Data(units, resolve, new AllScopes(scopes, grouped))
}
def expandDynamicDeps(deps: Seq[Any], structure: BuildStructure): Set[ScopedKey[?]] = {
if deps.isEmpty then Set.empty
else {
val data = dataFromStructure(structure)
val result = scala.collection.mutable.Set.empty[ScopedKey[?]]
for dep <- deps do
dep match
case (key: AttributeKey[?], filter: ScopeFilter) =>
try
filter(data).foreach(scope => result += Def.ScopedKey(scope, key))
catch
case _: Exception => ()
case _ =>
result.toSet
}
}
private[sbt] val allScopes: Initialize[AllScopes] = Def.setting {
val scopes = Def.StaticScopes.value
val grouped: ScopeMap =

View File

@ -135,7 +135,11 @@ object LintUnused {
val display = Def.showShortKey(None) // extracted.showKey
val comp = structure.compiledMap
val cMap = Def.flattenLocals(comp)
val used: Set[ScopedKey[?]] = cMap.values.flatMap(_.dependencies).toSet
val staticUsed: Set[ScopedKey[?]] = cMap.values.flatMap(_.dependencies).toSet
val dynamicDeps =
comp.values.flatMap(_.settings).flatMap(s => s.init.dynamicDependencies).toSeq
val dynamicUsed = ScopeFilter.expandDynamicDeps(dynamicDeps, structure)
val used: Set[ScopedKey[?]] = staticUsed ++ dynamicUsed
val unused: Seq[ScopedKey[?]] = cMap.keys.filter(!used.contains(_)).toSeq
val withDefinedAts: Seq[UnusedKey] = unused.map { u =>
val data = Project.scopedKeyData(structure, u)

View File

@ -630,6 +630,10 @@ trait Init:
*/
sealed trait Initialize[A1]:
def dependencies: Seq[ScopedKey[?]]
/** Dynamic dependencies (e.g. from .all(ScopeFilter)) not visible in the static graph. */
private[sbt] def dynamicDependencies: Seq[Any] = Nil
def apply[A2](g: A1 => A2): Initialize[A2]
private[sbt] def mapReferenced(g: MapScoped): Initialize[A1]
@ -999,6 +1003,32 @@ trait Init:
inputs.toList0.foldLeft(init) { (v, i) => i.processAttributes(v)(f) }
end Apply
private[sbt] final class DynamicDepsInitialize[A1](
inner: Initialize[A1],
val dynamicDeps: Seq[Any]
) extends Initialize[A1]:
override def dependencies: Seq[ScopedKey[?]] = inner.dependencies
override def dynamicDependencies: Seq[Any] = dynamicDeps
override def apply[A2](g: A1 => A2): Initialize[A2] =
DynamicDepsInitialize(inner.apply(g), dynamicDeps)
override def mapReferenced(g: MapScoped): Initialize[A1] =
DynamicDepsInitialize(inner.mapReferenced(g), dynamicDeps)
override def validateKeyReferenced(g: ValidateKeyRef): ValidatedInit[A1] =
inner.validateKeyReferenced(g).map(DynamicDepsInitialize(_, dynamicDeps))
override def mapConstant(g: MapConstant): Initialize[A1] =
DynamicDepsInitialize(inner.mapConstant(g), dynamicDeps)
override def evaluate(ss: Settings): A1 = inner.evaluate(ss)
private[sbt] override def processAttributes[A2](init: A2)(f: (A2, AttributeMap) => A2): A2 =
inner.processAttributes(init)(f)
end DynamicDepsInitialize
private[sbt] object DynamicDepsInitialize:
def apply[A1](inner: Initialize[A1], deps: Seq[Any]): Initialize[A1] =
if deps.isEmpty then inner else new DynamicDepsInitialize(inner, deps)
def withDynamicDependencies[A1](init: Initialize[A1], deps: Seq[Any]): Initialize[A1] =
DynamicDepsInitialize(init, deps)
private def remove[A](s: Seq[A], v: A) = s.filterNot(_ == v)
final class Undefined private[sbt] (val defining: Setting[?], val referencedKey: ScopedKey[?])