From 905862f60ce18bd67560c265d04e6445e3a87421 Mon Sep 17 00:00:00 2001 From: bitloi Date: Mon, 9 Feb 2026 15:55:12 +0100 Subject: [PATCH] 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. --- main/src/main/scala/sbt/ScopeFilter.scala | 59 +++++++++++++++++-- .../main/scala/sbt/internal/LintUnused.scala | 6 +- .../scala/sbt/internal/util/Settings.scala | 30 ++++++++++ 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/main/src/main/scala/sbt/ScopeFilter.scala b/main/src/main/scala/sbt/ScopeFilter.scala index f93d83f48..4d38508d7 100644 --- a/main/src/main/scala/sbt/ScopeFilter.scala +++ b/main/src/main/scala/sbt/ScopeFilter.scala @@ -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 = diff --git a/main/src/main/scala/sbt/internal/LintUnused.scala b/main/src/main/scala/sbt/internal/LintUnused.scala index 936caf31d..4fe5006bf 100644 --- a/main/src/main/scala/sbt/internal/LintUnused.scala +++ b/main/src/main/scala/sbt/internal/LintUnused.scala @@ -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) diff --git a/util-collection/src/main/scala/sbt/internal/util/Settings.scala b/util-collection/src/main/scala/sbt/internal/util/Settings.scala index 6295f5a04..2bc3f76cb 100644 --- a/util-collection/src/main/scala/sbt/internal/util/Settings.scala +++ b/util-collection/src/main/scala/sbt/internal/util/Settings.scala @@ -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[?])