From fe7b09d2d58baa834ed0fcb953f7a6831b0be13b Mon Sep 17 00:00:00 2001 From: Zach LaVallee Date: Tue, 12 May 2026 16:41:01 -0700 Subject: [PATCH 1/3] 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/3] 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/3] 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 +|]