From fe7b09d2d58baa834ed0fcb953f7a6831b0be13b Mon Sep 17 00:00:00 2001 From: Zach LaVallee Date: Tue, 12 May 2026 16:41:01 -0700 Subject: [PATCH 1/7] 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. --- Changelog.md | 4 ++ src/Strategy/Scala.hs | 18 +++--- src/Strategy/Scala/Plugin.hs | 103 ++++++++++++++++++++++++++--------- test/Scala/PluginSpec.hs | 81 +++++++++++++++++++++++---- 4 files changed, 162 insertions(+), 44 deletions(-) diff --git a/Changelog.md b/Changelog.md index 8a17f258e..af3289df9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,9 @@ # FOSSA CLI Changelog +## Unreleased + +- Scala/sbt: Run the uppercase `dependencyBrowseTreeHTML` task when the project explicitly enables `addDependencyTreePlugin` on sbt 1.4+. Previously the lowercase `dependencyBrowseTreeHtml` was used unconditionally for the explicit-plugin path, which sbt 1.4+ rejects, causing deep dependencies to be silently dropped. ([TKT-15490](https://fossa.atlassian.net/browse/ANE-2718)) + ## 3.17.5 - Vendetta: Debug bundles now include per-file component match data from Vendetta scans, making it easier to diagnose why a vendored dependency was or wasn't detected. ([#1706](https://github.com/fossas/fossa-cli/pull/1706)) diff --git a/src/Strategy/Scala.hs b/src/Strategy/Scala.hs index 6b85919f4..0835bd03f 100644 --- a/src/Strategy/Scala.hs +++ b/src/Strategy/Scala.hs @@ -60,7 +60,7 @@ import Strategy.Maven.Pom.PomFile (RawPom (rawPomArtifact, rawPomGroup, rawPomVe import Strategy.Maven.Pom.Resolver (buildGlobalClosure) import Strategy.Scala.Common (mkSbtCommand) import Strategy.Scala.Errors (FailedToListProjects (FailedToListProjects), MaybeWithoutDependencyTreeTask (..), MissingFullDependencyPlugin (..), sbtDepsGraphPluginUrl, scalaFossaDocUrl) -import Strategy.Scala.Plugin (genTreeJson, hasDependencyPlugins) +import Strategy.Scala.Plugin (DependencyPluginsDetected (..), genTreeJson, hasDependencyPlugins) import Strategy.Scala.SbtDependencyTree (SbtArtifact (SbtArtifact), analyze, sbtDepTreeCmd) import Strategy.Scala.SbtDependencyTreeJson qualified as TreeJson import Types ( @@ -161,10 +161,10 @@ findProjects = walkWithFilters' $ \dir _ files -> do . context ("Listing sbt projects at " <> pathToText dir) $ genPoms dir - (miniDepPlugin, depPlugin) <- hasDependencyPlugins dir - case (projectsRes, miniDepPlugin, depPlugin) of + DependencyPluginsDetected{hasMiniDependencyTreePlugin, dependencyTreePlugin} <- hasDependencyPlugins dir + case (projectsRes, hasMiniDependencyTreePlugin, dependencyTreePlugin) of (Nothing, _, _) -> pure ([], WalkSkipAll) - (Just projects, False, False) -> pure ([SbtTargets Nothing [] projects], WalkSkipAll) + (Just projects, False, Nothing) -> pure ([SbtTargets Nothing [] projects], WalkSkipAll) (Just projects, True, _) -> do -- project is using miniature dependency tree plugin, -- which is included by default with sbt 1.4+ @@ -184,9 +184,13 @@ findProjects = walkWithFilters' $ \dir _ files -> do (True, _) -> pure ([SbtTargets Nothing [] projects], WalkSkipAll) (_, Just _) -> pure ([SbtTargets depTreeStdOut [] projects], WalkSkipAll) (_, _) -> pure ([], WalkSkipAll) - (Just projects, False, True) -> do - -- project is explicitly configured to use dependency-tree-plugin - treeJSONs <- recover $ genTreeJson dir + (Just projects, False, Just pluginKind) -> do + -- project is explicitly configured to use dependency-tree-plugin. + -- The casing of the dependencyBrowseTree task differs between the + -- modern (sbt 1.4+) DependencyTreePlugin and the legacy + -- net.virtualvoid sbt-dependency-graph plugin; pluginKind selects + -- the right one. See TKT-15490 / ANE-2718. + treeJSONs <- recover $ genTreeJson pluginKind dir pure ([SbtTargets Nothing (fromMaybe [] treeJSONs) projects], WalkSkipAll) analyzeWithPoms :: (Has Diagnostics sig m) => ScalaProject -> m DependencyResults diff --git a/src/Strategy/Scala/Plugin.hs b/src/Strategy/Scala/Plugin.hs index d921b1781..c4a1c8a3d 100644 --- a/src/Strategy/Scala/Plugin.hs +++ b/src/Strategy/Scala/Plugin.hs @@ -4,6 +4,8 @@ module Strategy.Scala.Plugin ( hasDependencyPlugins, detectDependencyPlugins, genTreeJson, + DependencyTreePluginKind (..), + DependencyPluginsDetected (..), ) where import Control.Effect.Diagnostics (Diagnostics, fatalText) @@ -22,45 +24,96 @@ import Effect.Exec ( import Path (Abs, Dir, File, Path, mkRelFile, parent, parseAbsFile, ()) import Strategy.Scala.Common (mkSbtCommand) +-- | Which non-mini dependency-tree plugin (if any) the project has installed. +-- +-- The two plugins differ in the casing of their @dependencyBrowseTree@ task. +-- See 'mkDependencyBrowseTreeCmd' for the command names. +data DependencyTreePluginKind + = -- | @sbt.plugins.DependencyTreePlugin@. Built into sbt 1.4+ and enabled + -- explicitly via @addDependencyTreePlugin@ in @plugins.sbt@. Provides the + -- uppercase @dependencyBrowseTreeHTML@ task. + ModernDependencyTreePlugin + | -- | @net.virtualvoid.sbt.graph.DependencyGraphPlugin@. The third-party + -- @sbt-dependency-graph@ plugin used on sbt < 1.4. Provides the lowercase + -- @dependencyBrowseTreeHtml@ task. + LegacyDependencyGraphPlugin + deriving (Eq, Ord, Show) + +-- | What the @sbt plugins@ output told us about dependency-tree plugins. +data DependencyPluginsDetected = DependencyPluginsDetected + { hasMiniDependencyTreePlugin :: Bool + , dependencyTreePlugin :: Maybe DependencyTreePluginKind + } + deriving (Eq, Ord, Show) + -- | Returns list of plugins used by sbt. -- Ref: https://www.scala-sbt.org/1.x/docs/Plugins.html getPlugins :: Command getPlugins = mkSbtCommand "plugins" --- | Returns (hasMiniDependencyTreePlugin, hasDependencyTreePlugin) by running sbt plugins. -hasDependencyPlugins :: (Has Exec sig m, Has Diagnostics sig m) => Path Abs Dir -> m (Bool, Bool) +-- | Detect which dependency-tree plugins are loaded by running @sbt plugins@. +hasDependencyPlugins :: (Has Exec sig m, Has Diagnostics sig m) => Path Abs Dir -> m DependencyPluginsDetected hasDependencyPlugins projectDir = do stdoutText <- (TextLazy.toStrict . decodeUtf8) <$> context "Identifying plugins" (execThrow projectDir getPlugins) pure $ detectDependencyPlugins stdoutText --- | Detect dependency plugins from sbt plugins output. --- Returns (hasMiniDependencyTreePlugin, hasDependencyTreePlugin). -detectDependencyPlugins :: Text -> (Bool, Bool) +-- | Classify dependency-tree plugins from the @sbt plugins@ output. +-- +-- The plugin names mapped here: +-- +-- * @sbt.plugins.MiniDependencyTreePlugin@ — bundled with sbt 1.4+, gives +-- us the @dependencyTree@ task used by 'Strategy.Scala.SbtDependencyTree'. +-- * @sbt.plugins.DependencyTreePlugin@ — opt-in on sbt 1.4+ via +-- @addDependencyTreePlugin@. Provides the uppercase +-- @dependencyBrowseTreeHTML@ task. +-- * @net.virtualvoid.sbt.graph.DependencyGraphPlugin@ — third-party plugin +-- used on sbt < 1.4. Provides the lowercase @dependencyBrowseTreeHtml@ +-- task. +-- +-- When both modern and legacy non-mini plugins are present we prefer the +-- modern one (sbt 1.4+ wins) since legacy plugin presence on a modern sbt +-- typically means the user has both kinds of declarations in their build. +detectDependencyPlugins :: Text -> DependencyPluginsDetected detectDependencyPlugins stdoutText = - ( Text.count ".MiniDependencyTreePlugin" stdoutText > 0 - , Text.count ".DependencyTreePlugin" stdoutText > 0 - || Text.count "net.virtualvoid.sbt.graph.DependencyGraphPlugin" stdoutText > 0 -- sbt < 1.4 - ) + DependencyPluginsDetected + { hasMiniDependencyTreePlugin = Text.count ".MiniDependencyTreePlugin" stdoutText > 0 + , dependencyTreePlugin = + if Text.count "sbt.plugins.DependencyTreePlugin" stdoutText > 0 + then Just ModernDependencyTreePlugin + else + if Text.count "net.virtualvoid.sbt.graph.DependencyGraphPlugin" stdoutText > 0 + then Just LegacyDependencyGraphPlugin + else Nothing + } --- | Generates Dependency Trees. --- Ref: https://github.com/sbt/sbt/blob/master/main/src/main/scala/sbt/plugins/DependencyTreeSettings.scala#L101 --- --- This command unlike 'dependencyBrowseTree', does not open --- the browser when executed. +-- | The sbt task that writes @tree.html@/@tree.json@ alongside its dependency +-- output. Plugin name vs task casing: -- --- It writes following files in target directory: --- ./tree.json --- ./tree.html --- ./tree.data.js +-- * 'ModernDependencyTreePlugin' (sbt 1.4+, @addDependencyTreePlugin@) → +-- @dependencyBrowseTreeHTML@. +-- * 'LegacyDependencyGraphPlugin' (sbt < 1.4, @sbt-dependency-graph@) → +-- @dependencyBrowseTreeHtml@. -- --- This command is only used when the plugin is installed explicitly, i.e. sbt < 1.4. --- Newer versions of sbt will use the built-in dependency graph plugin. -mkDependencyBrowseTreeHTMLCmd :: Command -mkDependencyBrowseTreeHTMLCmd = mkSbtCommand "dependencyBrowseTreeHtml" +-- Picking the wrong casing produces an sbt error like +-- @[error] Not a valid command: dependencyBrowseTreeHTML@ which surfaces to +-- the user as "Could not retrieve output from sbt dependencyBrowseTreeHTML". +-- That regression (CLI 3.8.30) is tracked under TKT-15490 / ANE-2718. +mkDependencyBrowseTreeCmd :: DependencyTreePluginKind -> Command +mkDependencyBrowseTreeCmd ModernDependencyTreePlugin = mkSbtCommand "dependencyBrowseTreeHTML" +mkDependencyBrowseTreeCmd LegacyDependencyGraphPlugin = mkSbtCommand "dependencyBrowseTreeHtml" -genTreeJson :: (Has Exec sig m, Has Diagnostics sig m) => Path Abs Dir -> m [Path Abs File] -genTreeJson projectDir = do - stdoutBL <- context "Generating dependency tree html" $ execThrow projectDir mkDependencyBrowseTreeHTMLCmd +-- | Generates dependency trees by invoking the appropriate +-- @dependencyBrowseTree*@ task. Unlike @dependencyBrowseTree@, this does not +-- open a browser when executed. +-- +-- It writes the following files in the target directory: +-- +-- * @./tree.json@ +-- * @./tree.html@ +-- * @./tree.data.js@ +genTreeJson :: (Has Exec sig m, Has Diagnostics sig m) => DependencyTreePluginKind -> Path Abs Dir -> m [Path Abs File] +genTreeJson pluginKind projectDir = do + stdoutBL <- context "Generating dependency tree html" $ execThrow projectDir (mkDependencyBrowseTreeCmd pluginKind) -- stdout for "sbt dependencyBrowseTreeHTML" looks like: -- - diff --git a/test/Scala/PluginSpec.hs b/test/Scala/PluginSpec.hs index 61cf94447..8407444d8 100644 --- a/test/Scala/PluginSpec.hs +++ b/test/Scala/PluginSpec.hs @@ -5,7 +5,11 @@ module Scala.PluginSpec ( ) where import Data.Text (Text) -import Strategy.Scala.Plugin (detectDependencyPlugins) +import Strategy.Scala.Plugin ( + DependencyPluginsDetected (..), + DependencyTreePluginKind (..), + detectDependencyPlugins, + ) import Test.Hspec ( Spec, describe, @@ -18,20 +22,42 @@ spec :: Spec spec = do describe "detectDependencyPlugins" $ do it "should detect MiniDependencyTreePlugin (sbt 1.4+ built-in)" $ do - detectDependencyPlugins sbt14BuiltinOnly `shouldBe` (True, False) + detectDependencyPlugins sbt14BuiltinOnly + `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = True, dependencyTreePlugin = Nothing} - it "should detect explicit DependencyTreePlugin" $ do - detectDependencyPlugins sbtExplicitPluginOnly `shouldBe` (False, True) + it "should detect explicit modern DependencyTreePlugin (sbt 1.4+ addDependencyTreePlugin)" $ do + detectDependencyPlugins sbtModernExplicitPluginOnly + `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = False, dependencyTreePlugin = Just ModernDependencyTreePlugin} - it "should detect legacy net.virtualvoid plugin" $ do - detectDependencyPlugins sbtLegacyVirtualvoidPlugin `shouldBe` (False, True) + it "should detect legacy net.virtualvoid plugin (sbt < 1.4 sbt-dependency-graph)" $ do + detectDependencyPlugins sbtLegacyVirtualvoidPlugin + `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = False, dependencyTreePlugin = Just LegacyDependencyGraphPlugin} -- TKT-14742: When both plugins present, findProjects should prefer MiniDependencyTreePlugin - it "should detect both plugins when MiniDependencyTreePlugin AND explicit plugin present" $ do - detectDependencyPlugins sbt14WithExplicitPlugin `shouldBe` (True, True) + it "should detect both plugins when MiniDependencyTreePlugin AND modern explicit plugin present" $ do + detectDependencyPlugins sbt14WithExplicitPlugin + `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = True, dependencyTreePlugin = Just ModernDependencyTreePlugin} + + -- TKT-15490: sbt 1.11.5 with addDependencyTreePlugin and no auto-loaded + -- MiniDependencyTreePlugin must be classified as ModernDependencyTreePlugin + -- so the analyzer runs the uppercase `dependencyBrowseTreeHTML` task. The + -- pre-fix code returned (False, True) and the routing dispatched to the + -- legacy lowercase `dependencyBrowseTreeHtml`, which sbt 1.4+ rejects. + it "should classify modern DependencyTreePlugin alone as Modern (TKT-15490 routing guard)" $ do + let detected = detectDependencyPlugins sbt111ExplicitPluginOnly + hasMiniDependencyTreePlugin detected `shouldBe` False + dependencyTreePlugin detected `shouldBe` Just ModernDependencyTreePlugin + + -- If a project somehow lists both the modern and legacy plugin, prefer + -- the modern one — sbt 1.4+ wins, since the legacy plugin will not + -- function on a sbt that also surfaces sbt.plugins.DependencyTreePlugin. + it "should prefer modern DependencyTreePlugin when both modern and legacy are present" $ do + detectDependencyPlugins sbtBothModernAndLegacy + `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = False, dependencyTreePlugin = Just ModernDependencyTreePlugin} it "should detect no plugins when neither is present" $ do - detectDependencyPlugins sbtNoPlugins `shouldBe` (False, False) + detectDependencyPlugins sbtNoPlugins + `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = False, dependencyTreePlugin = Nothing} -- sbt 1.4+ with only built-in plugin sbt14BuiltinOnly :: Text @@ -49,9 +75,10 @@ sbt14BuiltinOnly = [info] sbt.plugins.SemanticdbPlugin: enabled in root |] --- sbt < 1.4 with explicit addDependencyTreePlugin -sbtExplicitPluginOnly :: Text -sbtExplicitPluginOnly = +-- sbt 1.4+ with explicit addDependencyTreePlugin and no MiniDependencyTreePlugin +-- listed (the case the customer in TKT-15490 hit on sbt 1.11.5). +sbtModernExplicitPluginOnly :: Text +sbtModernExplicitPluginOnly = [r|[info] welcome to sbt 1.3.13 (Eclipse Adoptium Java 11.0.21) [info] loading global plugins from /Users/test/.sbt/1.0/plugins [info] loading project definition from /Users/test/project/project @@ -64,6 +91,24 @@ sbtExplicitPluginOnly = [info] sbt.plugins.DependencyTreePlugin: enabled in root |] +-- sbt 1.11.5 with addDependencyTreePlugin in plugins.sbt — mirrors the +-- customer environment from TKT-15490. The customer reported that the +-- pre-fix CLI invoked the lowercase `dependencyBrowseTreeHtml`, which sbt +-- 1.4+ rejects. +sbt111ExplicitPluginOnly :: Text +sbt111ExplicitPluginOnly = + [r|[info] welcome to sbt 1.11.5 (Eclipse Adoptium Java 17.0.10) +[info] loading global plugins from /Users/test/.sbt/1.0/plugins +[info] loading project definition from /Users/test/project/project +[info] loading settings for project root from build.sbt ... +[info] set current project to test-project (in build file:/Users/test/project/) +[info] In file:/Users/test/project/ +[info] sbt.plugins.CorePlugin: enabled in root +[info] sbt.plugins.IvyPlugin: enabled in root +[info] sbt.plugins.JvmPlugin: enabled in root +[info] sbt.plugins.DependencyTreePlugin: enabled in root +|] + -- sbt < 1.4 with legacy net.virtualvoid plugin sbtLegacyVirtualvoidPlugin :: Text sbtLegacyVirtualvoidPlugin = @@ -96,6 +141,18 @@ sbt14WithExplicitPlugin = [info] sbt.plugins.SemanticdbPlugin: enabled in root |] +-- A pathological setup that lists both modern and legacy plugins. +sbtBothModernAndLegacy :: Text +sbtBothModernAndLegacy = + [r|[info] welcome to sbt 1.9.7 (Eclipse Adoptium Java 11.0.21) +[info] In file:/Users/test/project/ +[info] sbt.plugins.CorePlugin: enabled in root +[info] sbt.plugins.IvyPlugin: enabled in root +[info] sbt.plugins.JvmPlugin: enabled in root +[info] sbt.plugins.DependencyTreePlugin: enabled in root +[info] net.virtualvoid.sbt.graph.DependencyGraphPlugin: enabled in root +|] + sbtNoPlugins :: Text sbtNoPlugins = [r|[info] welcome to sbt 1.9.7 (Eclipse Adoptium Java 11.0.21) From 1ac512f05222157ff4ef2df3e4f2a20727311ef8 Mon Sep 17 00:00:00 2001 From: Zach LaVallee Date: Tue, 12 May 2026 17:23:40 -0700 Subject: [PATCH 2/7] 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 --- src/Strategy/Scala.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Strategy/Scala.hs b/src/Strategy/Scala.hs index 0835bd03f..dbcd3278f 100644 --- a/src/Strategy/Scala.hs +++ b/src/Strategy/Scala.hs @@ -203,13 +203,13 @@ analyzeWithPoms (ScalaProject _ _ closure) = context "Analyzing sbt dependencies } analyzeWithDepTreeJson :: (Has ReadFS sig m, Has Diagnostics sig m) => ScalaProject -> m DependencyResults -analyzeWithDepTreeJson (ScalaProject _ treeJson closure) = context "Analyzing sbt dependencies using dependencyBrowseTreeHTML" $ do +analyzeWithDepTreeJson (ScalaProject _ treeJson closure) = context "Analyzing sbt dependencies using dependency tree JSON" $ do treeJson' <- errCtx MissingFullDependencyPluginCtx $ errHelp MissingFullDependencyPluginHelp $ errDoc sbtDepsGraphPluginUrl $ errDoc scalaFossaDocUrl $ - fromMaybeText "Could not retrieve output from sbt dependencyBrowseTreeHTML" treeJson + fromMaybeText "Could not retrieve dependency tree JSON output from sbt" treeJson projectGraph <- TreeJson.analyze treeJson' pure $ DependencyResults From 12dfbb80d0dd46dfb9c55eac65715d2e3ecb35f5 Mon Sep 17 00:00:00 2001 From: Zachary LaVallee Date: Tue, 12 May 2026 23:04:00 -0700 Subject: [PATCH 3/7] Anchor sbt plugin detection on ": enabled in" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sbt plugins` lists user-disabled plugins (`disablePlugins(...)`) with a ": disabled in " 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 ": 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) --- src/Strategy/Scala/Plugin.hs | 12 +++++++++--- test/Scala/PluginSpec.hs | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/Strategy/Scala/Plugin.hs b/src/Strategy/Scala/Plugin.hs index c4a1c8a3d..cb514b750 100644 --- a/src/Strategy/Scala/Plugin.hs +++ b/src/Strategy/Scala/Plugin.hs @@ -70,18 +70,24 @@ hasDependencyPlugins projectDir = do -- used on sbt < 1.4. Provides the lowercase @dependencyBrowseTreeHtml@ -- task. -- +-- Detection anchors on the @\: enabled in@ suffix rather than the bare +-- FQCN. @sbt plugins@ lists plugins the user has explicitly disabled (via +-- @disablePlugins(...)@) with @: disabled in \@ — those still +-- contain the FQCN as a substring, so an unanchored match would wrongly +-- route to a task that doesn't exist on the active plugin set. +-- -- When both modern and legacy non-mini plugins are present we prefer the -- modern one (sbt 1.4+ wins) since legacy plugin presence on a modern sbt -- typically means the user has both kinds of declarations in their build. detectDependencyPlugins :: Text -> DependencyPluginsDetected detectDependencyPlugins stdoutText = DependencyPluginsDetected - { hasMiniDependencyTreePlugin = Text.count ".MiniDependencyTreePlugin" stdoutText > 0 + { hasMiniDependencyTreePlugin = "sbt.plugins.MiniDependencyTreePlugin: enabled in" `Text.isInfixOf` stdoutText , dependencyTreePlugin = - if Text.count "sbt.plugins.DependencyTreePlugin" stdoutText > 0 + if "sbt.plugins.DependencyTreePlugin: enabled in" `Text.isInfixOf` stdoutText then Just ModernDependencyTreePlugin else - if Text.count "net.virtualvoid.sbt.graph.DependencyGraphPlugin" stdoutText > 0 + if "net.virtualvoid.sbt.graph.DependencyGraphPlugin: enabled in" `Text.isInfixOf` stdoutText then Just LegacyDependencyGraphPlugin else Nothing } diff --git a/test/Scala/PluginSpec.hs b/test/Scala/PluginSpec.hs index 8407444d8..0c65c3dbe 100644 --- a/test/Scala/PluginSpec.hs +++ b/test/Scala/PluginSpec.hs @@ -59,6 +59,18 @@ spec = do detectDependencyPlugins sbtNoPlugins `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = False, dependencyTreePlugin = Nothing} + -- `sbt plugins` lists user-disabled plugins (`disablePlugins(...)`) with a + -- ": disabled in " suffix. The FQCN still appears on those lines, + -- so detection must anchor on ": enabled in" to avoid routing to a task + -- the active plugin set doesn't provide. + it "should not treat MiniDependencyTreePlugin as present when listed as disabled" $ do + detectDependencyPlugins sbtDisabledMiniPlugin + `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = False, dependencyTreePlugin = Nothing} + + it "should not treat modern DependencyTreePlugin as present when listed as disabled" $ do + detectDependencyPlugins sbtDisabledModernPlugin + `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = False, dependencyTreePlugin = Nothing} + -- sbt 1.4+ with only built-in plugin sbt14BuiltinOnly :: Text sbt14BuiltinOnly = @@ -165,3 +177,28 @@ sbtNoPlugins = [info] sbt.plugins.IvyPlugin: enabled in root [info] sbt.plugins.JvmPlugin: enabled in root |] + +-- User-disabled MiniDependencyTreePlugin (e.g. `disablePlugins(MiniDependencyTreePlugin)` +-- in build.sbt). The FQCN appears on a ": disabled in" line — detection +-- must reject it. +sbtDisabledMiniPlugin :: Text +sbtDisabledMiniPlugin = + [r|[info] welcome to sbt 1.9.7 (Eclipse Adoptium Java 11.0.21) +[info] In file:/Users/test/project/ +[info] sbt.plugins.CorePlugin: enabled in root +[info] sbt.plugins.IvyPlugin: enabled in root +[info] sbt.plugins.JvmPlugin: enabled in root +[info] sbt.plugins.MiniDependencyTreePlugin: disabled in root +|] + +-- User-disabled modern DependencyTreePlugin. Routing to the uppercase task +-- would fail because the plugin isn't active. +sbtDisabledModernPlugin :: Text +sbtDisabledModernPlugin = + [r|[info] welcome to sbt 1.11.5 (Eclipse Adoptium Java 17.0.10) +[info] In file:/Users/test/project/ +[info] sbt.plugins.CorePlugin: enabled in root +[info] sbt.plugins.IvyPlugin: enabled in root +[info] sbt.plugins.JvmPlugin: enabled in root +[info] sbt.plugins.DependencyTreePlugin: disabled in root +|] From 30e7640c11b443a5ba6df3a7241bf1ccff27afbc Mon Sep 17 00:00:00 2001 From: Zachary LaVallee Date: Thu, 18 Jun 2026 16:15:47 -0700 Subject: [PATCH 4/7] 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 --- src/Strategy/Scala/Plugin.hs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Strategy/Scala/Plugin.hs b/src/Strategy/Scala/Plugin.hs index cb514b750..35e159691 100644 --- a/src/Strategy/Scala/Plugin.hs +++ b/src/Strategy/Scala/Plugin.hs @@ -10,6 +10,7 @@ module Strategy.Scala.Plugin ( import Control.Effect.Diagnostics (Diagnostics, fatalText) import Control.Effect.Stack (context) +import Data.List (find) import Data.Maybe (mapMaybe) import Data.String.Conversion (ConvertUtf8 (decodeUtf8), toString) import Data.Text (Text) @@ -82,15 +83,15 @@ hasDependencyPlugins projectDir = do detectDependencyPlugins :: Text -> DependencyPluginsDetected detectDependencyPlugins stdoutText = DependencyPluginsDetected - { hasMiniDependencyTreePlugin = "sbt.plugins.MiniDependencyTreePlugin: enabled in" `Text.isInfixOf` stdoutText - , dependencyTreePlugin = - if "sbt.plugins.DependencyTreePlugin: enabled in" `Text.isInfixOf` stdoutText - then Just ModernDependencyTreePlugin - else - if "net.virtualvoid.sbt.graph.DependencyGraphPlugin: enabled in" `Text.isInfixOf` stdoutText - then Just LegacyDependencyGraphPlugin - else Nothing + { hasMiniDependencyTreePlugin = enabled "sbt.plugins.MiniDependencyTreePlugin" + , dependencyTreePlugin = snd <$> find (enabled . fst) treePlugins } + where + enabled name = (name <> ": enabled in") `Text.isInfixOf` stdoutText + treePlugins = + [ ("sbt.plugins.DependencyTreePlugin", ModernDependencyTreePlugin) + , ("net.virtualvoid.sbt.graph.DependencyGraphPlugin", LegacyDependencyGraphPlugin) + ] -- | The sbt task that writes @tree.html@/@tree.json@ alongside its dependency -- output. Plugin name vs task casing: From d6f7181d06f6978aed3a8a182deae6723d24a310 Mon Sep 17 00:00:00 2001 From: Zachary LaVallee Date: Thu, 18 Jun 2026 16:41:16 -0700 Subject: [PATCH 5/7] 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 --- Changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 85142ac52..72a9351f3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,7 +2,7 @@ ## Unreleased -- Scala/sbt: Run the uppercase `dependencyBrowseTreeHTML` task when the project explicitly enables `addDependencyTreePlugin` on sbt 1.4+. Previously the lowercase `dependencyBrowseTreeHtml` was used unconditionally for the explicit-plugin path, which sbt 1.4+ rejects, causing deep dependencies to be silently dropped. ([TKT-15490](https://fossa.atlassian.net/browse/ANE-2718)) +- Scala/sbt: Fix silently dropped deep dependencies on sbt 1.4+ with explicit `addDependencyTreePlugin`. ([#1711](https://github.com/fossas/fossa-cli/pull/1711)) ## 3.17.6 From 6eed7caa0e24d8b00d5dff47987979aee5eaa253 Mon Sep 17 00:00:00 2001 From: Zachary LaVallee Date: Thu, 18 Jun 2026 17:00:53 -0700 Subject: [PATCH 6/7] Strategy/Scala: detect plugins from real `sbt plugins` section layout The find-based detection refactor anchored on a ": enabled in" suffix that `sbt plugins` never prints. Real output (verified on sbt 1.9.8) lists bare plugin FQCNs under "Enabled plugins in :" 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 --- src/Strategy/Scala/Plugin.hs | 22 ++- test/Scala/PluginSpec.hs | 291 +++++++++++++++++++---------------- 2 files changed, 173 insertions(+), 140 deletions(-) diff --git a/src/Strategy/Scala/Plugin.hs b/src/Strategy/Scala/Plugin.hs index 35e159691..bd7641ff7 100644 --- a/src/Strategy/Scala/Plugin.hs +++ b/src/Strategy/Scala/Plugin.hs @@ -71,11 +71,19 @@ hasDependencyPlugins projectDir = do -- used on sbt < 1.4. Provides the lowercase @dependencyBrowseTreeHtml@ -- task. -- --- Detection anchors on the @\: enabled in@ suffix rather than the bare --- FQCN. @sbt plugins@ lists plugins the user has explicitly disabled (via --- @disablePlugins(...)@) with @: disabled in \@ — those still --- contain the FQCN as a substring, so an unanchored match would wrongly --- route to a task that doesn't exist on the active plugin set. +-- @sbt plugins@ groups its output into per-project @Enabled plugins in +-- \:@ sections, listing one bare plugin FQCN per indented line, +-- followed by a trailing @Plugins that are loaded to the build but not enabled +-- in any subprojects:@ section. A plugin the user disabled via +-- @disablePlugins(...)@ moves into that trailing section. We therefore search +-- only the text *before* that marker, so a disabled (loaded-but-not-enabled) +-- plugin is not mistaken for an active one. +-- +-- The plugin FQCN appears on its own line with no @: enabled in@ suffix. An +-- earlier attempt to anchor detection on such a suffix matched nothing in real +-- sbt output, which silently dropped deep dependencies and fell back to poms +-- (regressed TKT-15490). See @test/Scala/PluginSpec.hs@ for fixtures captured +-- from actual @sbt -batch -no-colors plugins@ runs. -- -- When both modern and legacy non-mini plugins are present we prefer the -- modern one (sbt 1.4+ wins) since legacy plugin presence on a modern sbt @@ -87,7 +95,9 @@ detectDependencyPlugins stdoutText = , dependencyTreePlugin = snd <$> find (enabled . fst) treePlugins } where - enabled name = (name <> ": enabled in") `Text.isInfixOf` stdoutText + enabledSection = fst $ Text.breakOn notEnabledMarker stdoutText + notEnabledMarker = "Plugins that are loaded to the build but not enabled" + enabled name = name `Text.isInfixOf` enabledSection treePlugins = [ ("sbt.plugins.DependencyTreePlugin", ModernDependencyTreePlugin) , ("net.virtualvoid.sbt.graph.DependencyGraphPlugin", LegacyDependencyGraphPlugin) diff --git a/test/Scala/PluginSpec.hs b/test/Scala/PluginSpec.hs index 0c65c3dbe..45a847eec 100644 --- a/test/Scala/PluginSpec.hs +++ b/test/Scala/PluginSpec.hs @@ -18,187 +18,210 @@ import Test.Hspec ( ) import Text.RawString.QQ (r) +-- The fixtures below mirror the real layout of @sbt -batch -no-colors plugins@: +-- per-project "Enabled plugins in :" sections list one bare plugin +-- FQCN per indented line, and a trailing "Plugins that are loaded to the build +-- but not enabled in any subprojects:" section lists the rest. There is no +-- ": enabled in " suffix in real output. The sbt 1.9.8 fixtures +-- (default, addDependencyTreePlugin, disabled-mini) are verbatim captures from +-- fossas/scala3-example-project; the remainder follow the same layout. spec :: Spec spec = do describe "detectDependencyPlugins" $ do - it "should detect MiniDependencyTreePlugin (sbt 1.4+ built-in)" $ do - detectDependencyPlugins sbt14BuiltinOnly + -- Regression guard for TKT-15490: this is the verbatim `sbt plugins` + -- output for fossas/scala3-example-project (sbt 1.9.8, no plugins.sbt). + -- The built-in MiniDependencyTreePlugin must be detected so the deep + -- `dependencyTree` path runs. A prior refactor anchored detection on a + -- non-existent ": enabled in" suffix, returned False here, and + -- silently fell back to generated poms (Partial graph). + it "detects the built-in MiniDependencyTreePlugin (sbt 1.9.8, no plugins.sbt)" $ do + detectDependencyPlugins sbt198DefaultBuiltin `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = True, dependencyTreePlugin = Nothing} - it "should detect explicit modern DependencyTreePlugin (sbt 1.4+ addDependencyTreePlugin)" $ do - detectDependencyPlugins sbtModernExplicitPluginOnly - `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = False, dependencyTreePlugin = Just ModernDependencyTreePlugin} + -- Real sbt 1.9.8 output with `addDependencyTreePlugin` in project/plugins.sbt + -- (the customer setup from TKT-15490). sbt enables the explicit + -- DependencyTreePlugin alongside the built-in MiniDependencyTreePlugin. + it "detects MiniDependencyTreePlugin and modern DependencyTreePlugin together (addDependencyTreePlugin)" $ do + detectDependencyPlugins sbt198AddDependencyTreePlugin + `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = True, dependencyTreePlugin = Just ModernDependencyTreePlugin} - it "should detect legacy net.virtualvoid plugin (sbt < 1.4 sbt-dependency-graph)" $ do + -- sbt < 1.4 with the third-party net.virtualvoid sbt-dependency-graph + -- plugin. MiniDependencyTreePlugin does not exist before sbt 1.4, so it is + -- absent; routing must dispatch to the legacy lowercase task. + it "detects the legacy net.virtualvoid plugin (sbt-dependency-graph)" $ do detectDependencyPlugins sbtLegacyVirtualvoidPlugin `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = False, dependencyTreePlugin = Just LegacyDependencyGraphPlugin} - -- TKT-14742: When both plugins present, findProjects should prefer MiniDependencyTreePlugin - it "should detect both plugins when MiniDependencyTreePlugin AND modern explicit plugin present" $ do - detectDependencyPlugins sbt14WithExplicitPlugin - `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = True, dependencyTreePlugin = Just ModernDependencyTreePlugin} + -- TKT-15490 routing guard: when the modern DependencyTreePlugin is enabled + -- but MiniDependencyTreePlugin is not, findProjects routes to genTreeJson, + -- which must select the uppercase `dependencyBrowseTreeHTML` task. The + -- classification must be Modern (not Legacy lowercase). + it "classifies a modern DependencyTreePlugin with no MiniDependencyTreePlugin as Modern" $ do + detectDependencyPlugins sbtModernWithoutMini + `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = False, dependencyTreePlugin = Just ModernDependencyTreePlugin} - -- TKT-15490: sbt 1.11.5 with addDependencyTreePlugin and no auto-loaded - -- MiniDependencyTreePlugin must be classified as ModernDependencyTreePlugin - -- so the analyzer runs the uppercase `dependencyBrowseTreeHTML` task. The - -- pre-fix code returned (False, True) and the routing dispatched to the - -- legacy lowercase `dependencyBrowseTreeHtml`, which sbt 1.4+ rejects. - it "should classify modern DependencyTreePlugin alone as Modern (TKT-15490 routing guard)" $ do - let detected = detectDependencyPlugins sbt111ExplicitPluginOnly - hasMiniDependencyTreePlugin detected `shouldBe` False - dependencyTreePlugin detected `shouldBe` Just ModernDependencyTreePlugin - - -- If a project somehow lists both the modern and legacy plugin, prefer - -- the modern one — sbt 1.4+ wins, since the legacy plugin will not - -- function on a sbt that also surfaces sbt.plugins.DependencyTreePlugin. - it "should prefer modern DependencyTreePlugin when both modern and legacy are present" $ do + -- If a build somehow enables both the modern and legacy plugin, prefer the + -- modern one — on sbt 1.4+ the legacy lowercase task is unavailable. + it "prefers modern DependencyTreePlugin when both modern and legacy are enabled" $ do detectDependencyPlugins sbtBothModernAndLegacy `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = False, dependencyTreePlugin = Just ModernDependencyTreePlugin} - it "should detect no plugins when neither is present" $ do + it "detects no dependency-tree plugins when none are enabled" $ do detectDependencyPlugins sbtNoPlugins `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = False, dependencyTreePlugin = Nothing} - -- `sbt plugins` lists user-disabled plugins (`disablePlugins(...)`) with a - -- ": disabled in " suffix. The FQCN still appears on those lines, - -- so detection must anchor on ": enabled in" to avoid routing to a task - -- the active plugin set doesn't provide. - it "should not treat MiniDependencyTreePlugin as present when listed as disabled" $ do - detectDependencyPlugins sbtDisabledMiniPlugin + -- `disablePlugins(MiniDependencyTreePlugin)` moves the plugin into the + -- trailing "loaded to the build but not enabled in any subprojects" + -- section. This is a verbatim sbt 1.9.8 capture; detection must not treat + -- it as active. An unanchored substring match (the pre-refactor behavior) + -- would wrongly count it. + it "ignores a MiniDependencyTreePlugin listed as loaded-but-not-enabled" $ do + detectDependencyPlugins sbt198DisabledMini `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = False, dependencyTreePlugin = Nothing} - it "should not treat modern DependencyTreePlugin as present when listed as disabled" $ do - detectDependencyPlugins sbtDisabledModernPlugin + it "ignores a modern DependencyTreePlugin listed as loaded-but-not-enabled" $ do + detectDependencyPlugins sbtDisabledModern `shouldBe` DependencyPluginsDetected{hasMiniDependencyTreePlugin = False, dependencyTreePlugin = Nothing} --- sbt 1.4+ with only built-in plugin -sbt14BuiltinOnly :: Text -sbt14BuiltinOnly = - [r|[info] welcome to sbt 1.9.7 (Eclipse Adoptium Java 11.0.21) -[info] loading global plugins from /Users/test/.sbt/1.0/plugins -[info] loading project definition from /Users/test/project/project +-- Verbatim `sbt -batch -no-colors plugins` output for fossas/scala3-example-project +-- (sbt 1.9.8, no project/plugins.sbt). +sbt198DefaultBuiltin :: Text +sbt198DefaultBuiltin = + [r|[info] welcome to sbt 1.9.8 (Homebrew Java 24.0.1) +[info] loading project definition from /private/tmp/scala3-example-project/project [info] loading settings for project root from build.sbt ... -[info] set current project to test-project (in build file:/Users/test/project/) -[info] In file:/Users/test/project/ -[info] sbt.plugins.CorePlugin: enabled in root -[info] sbt.plugins.IvyPlugin: enabled in root -[info] sbt.plugins.JvmPlugin: enabled in root -[info] sbt.plugins.MiniDependencyTreePlugin: enabled in root -[info] sbt.plugins.SemanticdbPlugin: enabled in root +[info] set current project to scala3-example-project (in build file:/private/tmp/scala3-example-project/) +In build /private/tmp/scala3-example-project/: + Enabled plugins in root: + sbt.plugins.CorePlugin + sbt.plugins.Giter8TemplatePlugin + sbt.plugins.IvyPlugin + sbt.plugins.JUnitXmlReportPlugin + sbt.plugins.JvmPlugin + sbt.plugins.MiniDependencyTreePlugin + sbt.plugins.SemanticdbPlugin +Plugins that are loaded to the build but not enabled in any subprojects: + sbt.ScriptedPlugin + sbt.plugins.SbtPlugin |] --- sbt 1.4+ with explicit addDependencyTreePlugin and no MiniDependencyTreePlugin --- listed (the case the customer in TKT-15490 hit on sbt 1.11.5). -sbtModernExplicitPluginOnly :: Text -sbtModernExplicitPluginOnly = - [r|[info] welcome to sbt 1.3.13 (Eclipse Adoptium Java 11.0.21) -[info] loading global plugins from /Users/test/.sbt/1.0/plugins -[info] loading project definition from /Users/test/project/project +-- Verbatim sbt 1.9.8 output with `addDependencyTreePlugin` added to +-- project/plugins.sbt (the TKT-15490 customer setup). +sbt198AddDependencyTreePlugin :: Text +sbt198AddDependencyTreePlugin = + [r|[info] welcome to sbt 1.9.8 (Homebrew Java 24.0.1) +[info] loading project definition from /private/tmp/scala3-example-project/project [info] loading settings for project root from build.sbt ... -[info] set current project to test-project (in build file:/Users/test/project/) -[info] In file:/Users/test/project/ -[info] sbt.plugins.CorePlugin: enabled in root -[info] sbt.plugins.IvyPlugin: enabled in root -[info] sbt.plugins.JvmPlugin: enabled in root -[info] sbt.plugins.DependencyTreePlugin: enabled in root +[info] set current project to scala3-example-project (in build file:/private/tmp/scala3-example-project/) +In build /private/tmp/scala3-example-project/: + Enabled plugins in root: + sbt.plugins.CorePlugin + sbt.plugins.DependencyTreePlugin + sbt.plugins.Giter8TemplatePlugin + sbt.plugins.IvyPlugin + sbt.plugins.JUnitXmlReportPlugin + sbt.plugins.JvmPlugin + sbt.plugins.MiniDependencyTreePlugin + sbt.plugins.SemanticdbPlugin +Plugins that are loaded to the build but not enabled in any subprojects: + sbt.ScriptedPlugin + sbt.plugins.SbtPlugin |] --- sbt 1.11.5 with addDependencyTreePlugin in plugins.sbt — mirrors the --- customer environment from TKT-15490. The customer reported that the --- pre-fix CLI invoked the lowercase `dependencyBrowseTreeHtml`, which sbt --- 1.4+ rejects. -sbt111ExplicitPluginOnly :: Text -sbt111ExplicitPluginOnly = - [r|[info] welcome to sbt 1.11.5 (Eclipse Adoptium Java 17.0.10) -[info] loading global plugins from /Users/test/.sbt/1.0/plugins -[info] loading project definition from /Users/test/project/project -[info] loading settings for project root from build.sbt ... -[info] set current project to test-project (in build file:/Users/test/project/) -[info] In file:/Users/test/project/ -[info] sbt.plugins.CorePlugin: enabled in root -[info] sbt.plugins.IvyPlugin: enabled in root -[info] sbt.plugins.JvmPlugin: enabled in root -[info] sbt.plugins.DependencyTreePlugin: enabled in root -|] - --- sbt < 1.4 with legacy net.virtualvoid plugin +-- sbt < 1.4 with the legacy net.virtualvoid sbt-dependency-graph plugin. sbtLegacyVirtualvoidPlugin :: Text sbtLegacyVirtualvoidPlugin = [r|[info] welcome to sbt 1.2.8 (Eclipse Adoptium Java 11.0.21) -[info] loading global plugins from /Users/test/.sbt/1.0/plugins -[info] loading project definition from /Users/test/project/project -[info] loading settings for project root from build.sbt ... [info] set current project to test-project (in build file:/Users/test/project/) -[info] In file:/Users/test/project/ -[info] sbt.plugins.CorePlugin: enabled in root -[info] sbt.plugins.IvyPlugin: enabled in root -[info] sbt.plugins.JvmPlugin: enabled in root -[info] net.virtualvoid.sbt.graph.DependencyGraphPlugin: enabled in root +In build /Users/test/project/: + Enabled plugins in root: + sbt.plugins.CorePlugin + sbt.plugins.IvyPlugin + sbt.plugins.JvmPlugin + net.virtualvoid.sbt.graph.DependencyGraphPlugin +Plugins that are loaded to the build but not enabled in any subprojects: + sbt.plugins.SbtPlugin |] --- TKT-14742: sbt 1.4+ with BOTH built-in and explicit plugin -sbt14WithExplicitPlugin :: Text -sbt14WithExplicitPlugin = - [r|[info] welcome to sbt 1.9.7 (Eclipse Adoptium Java 11.0.21) -[info] loading global plugins from /Users/test/.sbt/1.0/plugins -[info] loading project definition from /Users/test/project/project -[info] loading settings for project root from build.sbt ... +-- Modern DependencyTreePlugin enabled with MiniDependencyTreePlugin disabled +-- (`disablePlugins(MiniDependencyTreePlugin)` alongside addDependencyTreePlugin). +sbtModernWithoutMini :: Text +sbtModernWithoutMini = + [r|[info] welcome to sbt 1.11.5 (Eclipse Adoptium Java 17.0.10) [info] set current project to test-project (in build file:/Users/test/project/) -[info] In file:/Users/test/project/ -[info] sbt.plugins.CorePlugin: enabled in root -[info] sbt.plugins.IvyPlugin: enabled in root -[info] sbt.plugins.JvmPlugin: enabled in root -[info] sbt.plugins.MiniDependencyTreePlugin: enabled in root -[info] sbt.plugins.DependencyTreePlugin: enabled in root -[info] sbt.plugins.SemanticdbPlugin: enabled in root +In build /Users/test/project/: + Enabled plugins in root: + sbt.plugins.CorePlugin + sbt.plugins.DependencyTreePlugin + sbt.plugins.IvyPlugin + sbt.plugins.JvmPlugin +Plugins that are loaded to the build but not enabled in any subprojects: + sbt.plugins.MiniDependencyTreePlugin |] --- A pathological setup that lists both modern and legacy plugins. +-- A pathological build that enables both the modern and the legacy plugin. sbtBothModernAndLegacy :: Text sbtBothModernAndLegacy = [r|[info] welcome to sbt 1.9.7 (Eclipse Adoptium Java 11.0.21) -[info] In file:/Users/test/project/ -[info] sbt.plugins.CorePlugin: enabled in root -[info] sbt.plugins.IvyPlugin: enabled in root -[info] sbt.plugins.JvmPlugin: enabled in root -[info] sbt.plugins.DependencyTreePlugin: enabled in root -[info] net.virtualvoid.sbt.graph.DependencyGraphPlugin: enabled in root +[info] set current project to test-project (in build file:/Users/test/project/) +In build /Users/test/project/: + Enabled plugins in root: + sbt.plugins.CorePlugin + sbt.plugins.DependencyTreePlugin + sbt.plugins.IvyPlugin + sbt.plugins.JvmPlugin + net.virtualvoid.sbt.graph.DependencyGraphPlugin +Plugins that are loaded to the build but not enabled in any subprojects: + sbt.plugins.SbtPlugin |] sbtNoPlugins :: Text sbtNoPlugins = [r|[info] welcome to sbt 1.9.7 (Eclipse Adoptium Java 11.0.21) -[info] loading global plugins from /Users/test/.sbt/1.0/plugins -[info] loading project definition from /Users/test/project/project -[info] loading settings for project root from build.sbt ... [info] set current project to test-project (in build file:/Users/test/project/) -[info] In file:/Users/test/project/ -[info] sbt.plugins.CorePlugin: enabled in root -[info] sbt.plugins.IvyPlugin: enabled in root -[info] sbt.plugins.JvmPlugin: enabled in root +In build /Users/test/project/: + Enabled plugins in root: + sbt.plugins.CorePlugin + sbt.plugins.IvyPlugin + sbt.plugins.JvmPlugin +Plugins that are loaded to the build but not enabled in any subprojects: + sbt.plugins.SbtPlugin |] --- User-disabled MiniDependencyTreePlugin (e.g. `disablePlugins(MiniDependencyTreePlugin)` --- in build.sbt). The FQCN appears on a ": disabled in" line — detection --- must reject it. -sbtDisabledMiniPlugin :: Text -sbtDisabledMiniPlugin = - [r|[info] welcome to sbt 1.9.7 (Eclipse Adoptium Java 11.0.21) -[info] In file:/Users/test/project/ -[info] sbt.plugins.CorePlugin: enabled in root -[info] sbt.plugins.IvyPlugin: enabled in root -[info] sbt.plugins.JvmPlugin: enabled in root -[info] sbt.plugins.MiniDependencyTreePlugin: disabled in root +-- Verbatim sbt 1.9.8 output with `disablePlugins(sbt.plugins.MiniDependencyTreePlugin)` +-- on the root project. The plugin appears only in the trailing not-enabled +-- section. +sbt198DisabledMini :: Text +sbt198DisabledMini = + [r|[info] welcome to sbt 1.9.8 (Homebrew Java 24.0.1) +[info] loading project definition from /private/tmp/scala3-example-project/project +[info] loading settings for project root from build.sbt ... +[info] set current project to scala3-example-project (in build file:/private/tmp/scala3-example-project/) +In build /private/tmp/scala3-example-project/: + Enabled plugins in root: + sbt.plugins.CorePlugin + sbt.plugins.Giter8TemplatePlugin + sbt.plugins.IvyPlugin + sbt.plugins.JUnitXmlReportPlugin + sbt.plugins.JvmPlugin + sbt.plugins.SemanticdbPlugin +Plugins that are loaded to the build but not enabled in any subprojects: + sbt.ScriptedPlugin + sbt.plugins.MiniDependencyTreePlugin + sbt.plugins.SbtPlugin |] --- User-disabled modern DependencyTreePlugin. Routing to the uppercase task --- would fail because the plugin isn't active. -sbtDisabledModernPlugin :: Text -sbtDisabledModernPlugin = +-- Modern DependencyTreePlugin disabled via disablePlugins; it appears only in +-- the trailing not-enabled section, so routing must not target its task. +sbtDisabledModern :: Text +sbtDisabledModern = [r|[info] welcome to sbt 1.11.5 (Eclipse Adoptium Java 17.0.10) -[info] In file:/Users/test/project/ -[info] sbt.plugins.CorePlugin: enabled in root -[info] sbt.plugins.IvyPlugin: enabled in root -[info] sbt.plugins.JvmPlugin: enabled in root -[info] sbt.plugins.DependencyTreePlugin: disabled in root +[info] set current project to test-project (in build file:/Users/test/project/) +In build /Users/test/project/: + Enabled plugins in root: + sbt.plugins.CorePlugin + sbt.plugins.IvyPlugin + sbt.plugins.JvmPlugin +Plugins that are loaded to the build but not enabled in any subprojects: + sbt.plugins.DependencyTreePlugin |] From 36a9927a848384c830cfbd8ebb47f388c01e56ba Mon Sep 17 00:00:00 2001 From: Zachary LaVallee Date: Thu, 18 Jun 2026 18:20:38 -0700 Subject: [PATCH 7/7] 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 --- Changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 72a9351f3..288267db3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,7 +2,7 @@ ## Unreleased -- Scala/sbt: Fix silently dropped deep dependencies on sbt 1.4+ with explicit `addDependencyTreePlugin`. ([#1711](https://github.com/fossas/fossa-cli/pull/1711)) +- 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)) ## 3.17.6