Skip to content

Commit b5a1c53

Browse files
zlavclaude
andauthored
Strategy/Scala: route sbt 1.4+ with explicit DependencyTreePlugin to built-in command (TKT-15490) (#1711)
* Strategy/Scala: route sbt 1.4+ DependencyTreePlugin to uppercase HTML task When a project explicitly enables `addDependencyTreePlugin` on sbt 1.4+, fossa-cli detected `sbt.plugins.DependencyTreePlugin` and dispatched to the same path used by the legacy `net.virtualvoid.sbt.graph.DependencyGraphPlugin`, which runs the lowercase `dependencyBrowseTreeHtml` task. sbt 1.4+ only exposes the uppercase `dependencyBrowseTreeHTML`, so the task failed and the analyzer silently dropped deep dependencies. Distinguish the two plugins at detection time and pick the correct task casing per plugin. TKT-15490 / ANE-2718. * Strategy/Scala: make analyzeWithDepTreeJson messages task-agnostic The context label and error text mentioned dependencyBrowseTreeHTML, but the routing change can also pick the legacy dependencyBrowseTreeHtml task. Reword to "dependency tree JSON" so the message is correct in both cases. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Anchor sbt plugin detection on ": enabled in" `sbt plugins` lists user-disabled plugins (`disablePlugins(...)`) with a ": disabled in <scope>" suffix. The pre-existing substring match on the bare FQCN counted those as present, which routed `findProjects` to a task the active plugin set does not provide — same shape as the TKT-15490 regression, different trigger. Detection now requires "<FQCN>: enabled in" verbatim. Fixtures cover the two disabled cases (mini disabled, modern disabled), both expected to classify as Nothing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Strategy/Scala: refactor detectDependencyPlugins to find-based lookup Replace the nested if-then-else with a `find` over a precedence-ordered lookup table. Behavior is identical: `find` returns the first match so ModernDependencyTreePlugin still wins over Legacy, and the shared ": enabled in" suffix keeps the three effective marker strings unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Changelog: condense Unreleased Scala/sbt entry to one line Match the format of older entries and link the PR instead of the ticket. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Strategy/Scala: detect plugins from real `sbt plugins` section layout The find-based detection refactor anchored on a "<FQCN>: enabled in" suffix that `sbt plugins` never prints. Real output (verified on sbt 1.9.8) lists bare plugin FQCNs under "Enabled plugins in <project>:" sections, with disabled plugins moved to a trailing "Plugins that are loaded to the build but not enabled in any subprojects:" section. The suffix anchor matched nothing, so the built-in MiniDependencyTreePlugin went undetected on sbt 1.4+ projects with no plugins.sbt (e.g. scala3-example-project): the deep `dependencyTree` path never ran and analysis fell back to generated poms (Partial graph), failing the Analysis.Scala.scalaExampleProject integration test. Detect a plugin by searching for its FQCN in the text before the not-enabled marker, which preserves the disablePlugins guard. Replace the fabricated unit fixtures with verbatim captures from `sbt -batch -no-colors plugins`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Changelog: reword sbt 1.4+ entry around task-casing routing "silently" was inaccurate: NonStrict emitted a MissingDeepDeps warning before falling back to poms (Strict errored), so the drop was never silent. The real fix is routing to the uppercase dependencyBrowseTreeHTML task. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent ca6ed67 commit b5a1c53

4 files changed

Lines changed: 294 additions & 99 deletions

File tree

Changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# FOSSA CLI Changelog
22

3+
## Unreleased
4+
5+
- Scala/sbt: Route projects using `addDependencyTreePlugin` (sbt 1.4+) to the correct `dependencyBrowseTreeHTML` task, restoring deep dependencies. ([#1711](https://github.com/fossas/fossa-cli/pull/1711))
6+
37
## 3.17.11
48

59
- Conan: Handle list-valued license in make_fossa_deps_conan ([#1719](https://github.com/fossas/fossa-cli/pull/1719)).

src/Strategy/Scala.hs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ import Strategy.Maven.Pom.PomFile (RawPom (rawPomArtifact, rawPomGroup, rawPomVe
6060
import Strategy.Maven.Pom.Resolver (buildGlobalClosure)
6161
import Strategy.Scala.Common (mkSbtCommand)
6262
import Strategy.Scala.Errors (FailedToListProjects (FailedToListProjects), MaybeWithoutDependencyTreeTask (..), MissingFullDependencyPlugin (..), sbtDepsGraphPluginUrl, scalaFossaDocUrl)
63-
import Strategy.Scala.Plugin (genTreeJson, hasDependencyPlugins)
63+
import Strategy.Scala.Plugin (DependencyPluginsDetected (..), genTreeJson, hasDependencyPlugins)
6464
import Strategy.Scala.SbtDependencyTree (SbtArtifact (SbtArtifact), analyze, sbtDepTreeCmd)
6565
import Strategy.Scala.SbtDependencyTreeJson qualified as TreeJson
6666
import Types (
@@ -161,10 +161,10 @@ findProjects = walkWithFilters' $ \dir _ files -> do
161161
. context ("Listing sbt projects at " <> pathToText dir)
162162
$ genPoms dir
163163

164-
(miniDepPlugin, depPlugin) <- hasDependencyPlugins dir
165-
case (projectsRes, miniDepPlugin, depPlugin) of
164+
DependencyPluginsDetected{hasMiniDependencyTreePlugin, dependencyTreePlugin} <- hasDependencyPlugins dir
165+
case (projectsRes, hasMiniDependencyTreePlugin, dependencyTreePlugin) of
166166
(Nothing, _, _) -> pure ([], WalkSkipAll)
167-
(Just projects, False, False) -> pure ([SbtTargets Nothing [] projects], WalkSkipAll)
167+
(Just projects, False, Nothing) -> pure ([SbtTargets Nothing [] projects], WalkSkipAll)
168168
(Just projects, True, _) -> do
169169
-- project is using miniature dependency tree plugin,
170170
-- which is included by default with sbt 1.4+
@@ -184,9 +184,13 @@ findProjects = walkWithFilters' $ \dir _ files -> do
184184
(True, _) -> pure ([SbtTargets Nothing [] projects], WalkSkipAll)
185185
(_, Just _) -> pure ([SbtTargets depTreeStdOut [] projects], WalkSkipAll)
186186
(_, _) -> pure ([], WalkSkipAll)
187-
(Just projects, False, True) -> do
188-
-- project is explicitly configured to use dependency-tree-plugin
189-
treeJSONs <- recover $ genTreeJson dir
187+
(Just projects, False, Just pluginKind) -> do
188+
-- project is explicitly configured to use dependency-tree-plugin.
189+
-- The casing of the dependencyBrowseTree task differs between the
190+
-- modern (sbt 1.4+) DependencyTreePlugin and the legacy
191+
-- net.virtualvoid sbt-dependency-graph plugin; pluginKind selects
192+
-- the right one. See TKT-15490 / ANE-2718.
193+
treeJSONs <- recover $ genTreeJson pluginKind dir
190194
pure ([SbtTargets Nothing (fromMaybe [] treeJSONs) projects], WalkSkipAll)
191195

192196
analyzeWithPoms :: (Has Diagnostics sig m) => ScalaProject -> m DependencyResults
@@ -199,13 +203,13 @@ analyzeWithPoms (ScalaProject _ _ closure) = context "Analyzing sbt dependencies
199203
}
200204

201205
analyzeWithDepTreeJson :: (Has ReadFS sig m, Has Diagnostics sig m) => ScalaProject -> m DependencyResults
202-
analyzeWithDepTreeJson (ScalaProject _ treeJson closure) = context "Analyzing sbt dependencies using dependencyBrowseTreeHTML" $ do
206+
analyzeWithDepTreeJson (ScalaProject _ treeJson closure) = context "Analyzing sbt dependencies using dependency tree JSON" $ do
203207
treeJson' <-
204208
errCtx MissingFullDependencyPluginCtx $
205209
errHelp MissingFullDependencyPluginHelp $
206210
errDoc sbtDepsGraphPluginUrl $
207211
errDoc scalaFossaDocUrl $
208-
fromMaybeText "Could not retrieve output from sbt dependencyBrowseTreeHTML" treeJson
212+
fromMaybeText "Could not retrieve dependency tree JSON output from sbt" treeJson
209213
projectGraph <- TreeJson.analyze treeJson'
210214
pure $
211215
DependencyResults

src/Strategy/Scala/Plugin.hs

Lines changed: 95 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ module Strategy.Scala.Plugin (
44
hasDependencyPlugins,
55
detectDependencyPlugins,
66
genTreeJson,
7+
DependencyTreePluginKind (..),
8+
DependencyPluginsDetected (..),
79
) where
810

911
import Control.Effect.Diagnostics (Diagnostics, fatalText)
1012
import Control.Effect.Stack (context)
13+
import Data.List (find)
1114
import Data.Maybe (mapMaybe)
1215
import Data.String.Conversion (ConvertUtf8 (decodeUtf8), toString)
1316
import Data.Text (Text)
@@ -22,45 +25,112 @@ import Effect.Exec (
2225
import Path (Abs, Dir, File, Path, mkRelFile, parent, parseAbsFile, (</>))
2326
import Strategy.Scala.Common (mkSbtCommand)
2427

28+
-- | Which non-mini dependency-tree plugin (if any) the project has installed.
29+
--
30+
-- The two plugins differ in the casing of their @dependencyBrowseTree@ task.
31+
-- See 'mkDependencyBrowseTreeCmd' for the command names.
32+
data DependencyTreePluginKind
33+
= -- | @sbt.plugins.DependencyTreePlugin@. Built into sbt 1.4+ and enabled
34+
-- explicitly via @addDependencyTreePlugin@ in @plugins.sbt@. Provides the
35+
-- uppercase @dependencyBrowseTreeHTML@ task.
36+
ModernDependencyTreePlugin
37+
| -- | @net.virtualvoid.sbt.graph.DependencyGraphPlugin@. The third-party
38+
-- @sbt-dependency-graph@ plugin used on sbt < 1.4. Provides the lowercase
39+
-- @dependencyBrowseTreeHtml@ task.
40+
LegacyDependencyGraphPlugin
41+
deriving (Eq, Ord, Show)
42+
43+
-- | What the @sbt plugins@ output told us about dependency-tree plugins.
44+
data DependencyPluginsDetected = DependencyPluginsDetected
45+
{ hasMiniDependencyTreePlugin :: Bool
46+
, dependencyTreePlugin :: Maybe DependencyTreePluginKind
47+
}
48+
deriving (Eq, Ord, Show)
49+
2550
-- | Returns list of plugins used by sbt.
2651
-- Ref: https://www.scala-sbt.org/1.x/docs/Plugins.html
2752
getPlugins :: Command
2853
getPlugins = mkSbtCommand "plugins"
2954

30-
-- | Returns (hasMiniDependencyTreePlugin, hasDependencyTreePlugin) by running sbt plugins.
31-
hasDependencyPlugins :: (Has Exec sig m, Has Diagnostics sig m) => Path Abs Dir -> m (Bool, Bool)
55+
-- | Detect which dependency-tree plugins are loaded by running @sbt plugins@.
56+
hasDependencyPlugins :: (Has Exec sig m, Has Diagnostics sig m) => Path Abs Dir -> m DependencyPluginsDetected
3257
hasDependencyPlugins projectDir = do
3358
stdoutText <- (TextLazy.toStrict . decodeUtf8) <$> context "Identifying plugins" (execThrow projectDir getPlugins)
3459
pure $ detectDependencyPlugins stdoutText
3560

36-
-- | Detect dependency plugins from sbt plugins output.
37-
-- Returns (hasMiniDependencyTreePlugin, hasDependencyTreePlugin).
38-
detectDependencyPlugins :: Text -> (Bool, Bool)
61+
-- | Classify dependency-tree plugins from the @sbt plugins@ output.
62+
--
63+
-- The plugin names mapped here:
64+
--
65+
-- * @sbt.plugins.MiniDependencyTreePlugin@ — bundled with sbt 1.4+, gives
66+
-- us the @dependencyTree@ task used by 'Strategy.Scala.SbtDependencyTree'.
67+
-- * @sbt.plugins.DependencyTreePlugin@ — opt-in on sbt 1.4+ via
68+
-- @addDependencyTreePlugin@. Provides the uppercase
69+
-- @dependencyBrowseTreeHTML@ task.
70+
-- * @net.virtualvoid.sbt.graph.DependencyGraphPlugin@ — third-party plugin
71+
-- used on sbt < 1.4. Provides the lowercase @dependencyBrowseTreeHtml@
72+
-- task.
73+
--
74+
-- @sbt plugins@ groups its output into per-project @Enabled plugins in
75+
-- \<project\>:@ sections, listing one bare plugin FQCN per indented line,
76+
-- followed by a trailing @Plugins that are loaded to the build but not enabled
77+
-- in any subprojects:@ section. A plugin the user disabled via
78+
-- @disablePlugins(...)@ moves into that trailing section. We therefore search
79+
-- only the text *before* that marker, so a disabled (loaded-but-not-enabled)
80+
-- plugin is not mistaken for an active one.
81+
--
82+
-- The plugin FQCN appears on its own line with no @: enabled in@ suffix. An
83+
-- earlier attempt to anchor detection on such a suffix matched nothing in real
84+
-- sbt output, which silently dropped deep dependencies and fell back to poms
85+
-- (regressed TKT-15490). See @test/Scala/PluginSpec.hs@ for fixtures captured
86+
-- from actual @sbt -batch -no-colors plugins@ runs.
87+
--
88+
-- When both modern and legacy non-mini plugins are present we prefer the
89+
-- modern one (sbt 1.4+ wins) since legacy plugin presence on a modern sbt
90+
-- typically means the user has both kinds of declarations in their build.
91+
detectDependencyPlugins :: Text -> DependencyPluginsDetected
3992
detectDependencyPlugins stdoutText =
40-
( Text.count ".MiniDependencyTreePlugin" stdoutText > 0
41-
, Text.count ".DependencyTreePlugin" stdoutText > 0
42-
|| Text.count "net.virtualvoid.sbt.graph.DependencyGraphPlugin" stdoutText > 0 -- sbt < 1.4
43-
)
93+
DependencyPluginsDetected
94+
{ hasMiniDependencyTreePlugin = enabled "sbt.plugins.MiniDependencyTreePlugin"
95+
, dependencyTreePlugin = snd <$> find (enabled . fst) treePlugins
96+
}
97+
where
98+
enabledSection = fst $ Text.breakOn notEnabledMarker stdoutText
99+
notEnabledMarker = "Plugins that are loaded to the build but not enabled"
100+
enabled name = name `Text.isInfixOf` enabledSection
101+
treePlugins =
102+
[ ("sbt.plugins.DependencyTreePlugin", ModernDependencyTreePlugin)
103+
, ("net.virtualvoid.sbt.graph.DependencyGraphPlugin", LegacyDependencyGraphPlugin)
104+
]
44105

45-
-- | Generates Dependency Trees.
46-
-- Ref: https://github.com/sbt/sbt/blob/master/main/src/main/scala/sbt/plugins/DependencyTreeSettings.scala#L101
47-
--
48-
-- This command unlike 'dependencyBrowseTree', does not open
49-
-- the browser when executed.
106+
-- | The sbt task that writes @tree.html@/@tree.json@ alongside its dependency
107+
-- output. Plugin name vs task casing:
50108
--
51-
-- It writes following files in target directory:
52-
-- ./tree.json
53-
-- ./tree.html
54-
-- ./tree.data.js
109+
-- * 'ModernDependencyTreePlugin' (sbt 1.4+, @addDependencyTreePlugin@) →
110+
-- @dependencyBrowseTreeHTML@.
111+
-- * 'LegacyDependencyGraphPlugin' (sbt < 1.4, @sbt-dependency-graph@) →
112+
-- @dependencyBrowseTreeHtml@.
55113
--
56-
-- This command is only used when the plugin is installed explicitly, i.e. sbt < 1.4.
57-
-- Newer versions of sbt will use the built-in dependency graph plugin.
58-
mkDependencyBrowseTreeHTMLCmd :: Command
59-
mkDependencyBrowseTreeHTMLCmd = mkSbtCommand "dependencyBrowseTreeHtml"
114+
-- Picking the wrong casing produces an sbt error like
115+
-- @[error] Not a valid command: dependencyBrowseTreeHTML@ which surfaces to
116+
-- the user as "Could not retrieve output from sbt dependencyBrowseTreeHTML".
117+
-- That regression (CLI 3.8.30) is tracked under TKT-15490 / ANE-2718.
118+
mkDependencyBrowseTreeCmd :: DependencyTreePluginKind -> Command
119+
mkDependencyBrowseTreeCmd ModernDependencyTreePlugin = mkSbtCommand "dependencyBrowseTreeHTML"
120+
mkDependencyBrowseTreeCmd LegacyDependencyGraphPlugin = mkSbtCommand "dependencyBrowseTreeHtml"
60121

61-
genTreeJson :: (Has Exec sig m, Has Diagnostics sig m) => Path Abs Dir -> m [Path Abs File]
62-
genTreeJson projectDir = do
63-
stdoutBL <- context "Generating dependency tree html" $ execThrow projectDir mkDependencyBrowseTreeHTMLCmd
122+
-- | Generates dependency trees by invoking the appropriate
123+
-- @dependencyBrowseTree*@ task. Unlike @dependencyBrowseTree@, this does not
124+
-- open a browser when executed.
125+
--
126+
-- It writes the following files in the target directory:
127+
--
128+
-- * @./tree.json@
129+
-- * @./tree.html@
130+
-- * @./tree.data.js@
131+
genTreeJson :: (Has Exec sig m, Has Diagnostics sig m) => DependencyTreePluginKind -> Path Abs Dir -> m [Path Abs File]
132+
genTreeJson pluginKind projectDir = do
133+
stdoutBL <- context "Generating dependency tree html" $ execThrow projectDir (mkDependencyBrowseTreeCmd pluginKind)
64134

65135
-- stdout for "sbt dependencyBrowseTreeHTML" looks like:
66136
-- -

0 commit comments

Comments
 (0)