Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Maven: Static analysis of `pom.xml` files now reports the dependencies a project declares as direct. Previously the project itself was reported as the only direct dependency and everything it declared was reported as transitive, and in a multi-module project the root pom and every submodule were also reported as dependencies. This affects analysis without `mvn` on `PATH`, `--static-only-analysis`, and container scanning; analysis using `mvn` was already correct. Selecting a submodule with `--only-target` now reports that submodule's dependencies as direct instead of reporting none at all. ([#1768](https://github.com/fossas/fossa-cli/pull/1768))
- Diagnostics: When an error or warning group contains multiple errors, each error's `Traceback:` header is now printed on its own line instead of being glued onto the last line of the preceding error message (e.g. `...none passed validationTraceback:`). ([#1758](https://github.com/fossas/fossa-cli/pull/1758))

## 3.18.2
Expand Down
11 changes: 11 additions & 0 deletions docs/references/strategies/languages/maven/pomxml.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,15 @@ poms are linked together by their `<parent>` references into multi-project proje

Each project in the single- or multi-project structure has its pom information overlayed on top of parent poms, and a dependency graph is gathered from each project.

The root pom and every submodule are excluded from the reported graph, and the
dependencies they declare are reported as direct. This is the same treatment the
`mavenplugin` and `treecmd` tactics give them: those are the user's own projects,
not things they depend on. A pom file only declares direct dependencies, so this
tactic reports no transitive dependencies and no edges.

For example, given a project `com.example:root` whose `mod-a` submodule declares
`junit:junit` and whose `mod-b` submodule declares `com.google.guava:guava`, the
reported dependencies are `junit:junit` and `com.google.guava:guava`, both direct.
Neither `com.example:root` nor either submodule is reported.

We have limited support for naive `${property}` interpolation.
31 changes: 28 additions & 3 deletions src/Strategy/Maven.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module Strategy.Maven (
mkProject,
MavenProject (..),
getDeps,
getDepsStatically,
) where

import App.Fossa.Analyze.LicenseAnalyze (LicenseAnalyzeProject, licenseAnalyzeProject)
Expand All @@ -18,14 +19,14 @@ import Data.Set (Set)
import Data.Set qualified as Set
import Data.Set.NonEmpty (nonEmpty, toSet)
import Data.Text hiding (group, map)
import DepTypes (Dependency)
import DepTypes (Dependency (dependencyName))
import Diag.Common (MissingDeepDeps (MissingDeepDeps), MissingEdges (MissingEdges))
import Discovery.Filters (AllFilters, MavenScopeFilters, mavenScopeFilterSet)
import Discovery.Simple (simpleDiscover)
import Effect.Exec (CandidateCommandEffs, GetDepsEffs)
import Effect.ReadFS (ReadFS)
import GHC.Generics (Generic)
import Graphing (Graphing, gmap, shrinkRoots)
import Graphing (Graphing, gmap, promoteToDirect, shrink, shrinkRoots)
import Path (Abs, Dir, Path, parent)
import Strategy.Maven.Common (MavenDependency (..), filterMavenDependencyByScope, filterMavenSubmodules, mavenDependencyToDependency)
import Strategy.Maven.DepTree qualified as DepTreeCmd
Expand Down Expand Up @@ -188,7 +189,31 @@ getStaticAnalysis submoduleTargets closure = do
let allSubmodules = PomClosure.closureSubmodules closure
(graph, graphBreadth) <- context "Static analysis" $ pure (Pom.analyze' closure, Partial)
filteredGraph <- applyMavenFilters submoduleTargets allSubmodules graph
pure (filteredGraph, graphBreadth)
pure (withoutProjectsAsDeps allSubmodules filteredGraph, graphBreadth)

-- | Remove the user's own packages -- the root pom and every submodule -- from a
-- static graph, promoting the dependencies they declare to direct.
--
-- 'Pom.analyze'' builds a graph rooted at the project itself, so without this the
-- project is the only direct dependency and every dependency it declares is
-- reported as transitive. Marking the project packages direct and then shrinking
-- them away is the same two steps the dynamic tactics take, where
-- 'Plugin.buildGraph' marks them and 'shrinkRoots' removes them.
--
-- Both steps are needed. 'shrinkRoots' alone would remove only the root pom and
-- promote the submodules to direct dependencies rather than dropping them. A
-- 'shrink' alone would leave a submodule-filtered graph with no direct
-- dependencies at all, because submodule filtering removes the root pom node
-- without rewiring the edges through it.
--
-- This runs after submodule filtering, which needs the submodule nodes present in
-- the graph to work out which dependencies belong to which submodule.
withoutProjectsAsDeps :: Set Text -> Graphing Dependency -> Graphing Dependency
withoutProjectsAsDeps projectPackages =
shrink (not . isProjectPackage) . promoteToDirect isProjectPackage
where
isProjectPackage :: Dependency -> Bool
isProjectPackage dep = dependencyName dep `Set.member` projectPackages

applyMavenFilters :: (Has Diagnostics sig m, Has (Reader MavenScopeFilters) sig m) => Set Text -> Set Text -> Graphing MavenDependency -> m (Graphing Dependency)
applyMavenFilters targetSet submoduleSet graph = do
Expand Down
183 changes: 183 additions & 0 deletions test/Maven/PomStrategySpec.hs
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
{-# LANGUAGE QuasiQuotes #-}

module Maven.PomStrategySpec (
spec,
) where

import Control.Carrier.Reader (runReader)
import Control.Effect.Lift (sendIO)
import Data.ByteString.Char8 qualified as BS
import Data.List (sort)
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Set.NonEmpty (nonEmpty)
import Data.Text (Text)
import DepTypes (Dependency (dependencyName))
import Discovery.Filters (MavenScopeFilters (MavenScopeIncludeFilters))
import Graphing (Graphing)
import Graphing qualified
import Path (Abs, Dir, File, Path, Rel, reldir, relfile, toFilePath, (</>))
import Path.IO qualified as PIO
import Strategy.Maven (MavenProject (MavenProject), getDepsStatically, mkProject)
import Strategy.Maven.Pom (MavenPackage (..), buildMavenPackage, interpolateProperties)
import Strategy.Maven.Pom.Closure (findProjects)
import Strategy.Maven.Pom.PomFile
import Test.Effect (EffectStack, expectationFailure', itWithTempDir', shouldBe')
import Test.Hspec
import Types (BuildTarget (BuildTarget), DependencyResults (dependencyGraph), DiscoveredProject (projectBuildTargets), FoundTargets (FoundTargets, ProjectWithoutTargets))

spec :: Spec
spec = do
Expand Down Expand Up @@ -50,3 +69,167 @@ spec = do
}
)
result `shouldBe` MavenPackage "MYGROUP" "MYARTIFACT" (Just "MYVERSION")

-- The pom tactic builds its graph rooted at the project itself. These tests pin
-- that the project's own packages are removed from the reported graph and the
-- dependencies they declare are reported as direct, matching what the dynamic
-- tactics report for the same project.
describe "static analysis of a single-module project" $ do
itWithTempDir' "reports the declared dependencies as direct rather than the project itself" $ \dir -> do
writePom dir [relfile|pom.xml|] singleModulePom
onStaticGraph dir allTargets $ \graph ->
directNames graph `shouldBe'` ["junit:junit", "org.apache.commons:commons-lang3"]

itWithTempDir' "does not report the project itself as a dependency" $ \dir -> do
writePom dir [relfile|pom.xml|] singleModulePom
onStaticGraph dir allTargets $ \graph ->
vertexNames graph `shouldBe'` ["junit:junit", "org.apache.commons:commons-lang3"]

describe "static analysis of a multi-module project" $ do
-- commons-lang3 is declared by the root pom, so both submodules inherit it.
itWithTempDir' "reports the dependencies of every module as direct" $ \dir -> do
createMultiModuleFixture dir
onStaticGraph dir allTargets $ \graph ->
directNames graph `shouldBe'` ["com.google.guava:guava", "junit:junit", "org.apache.commons:commons-lang3"]

itWithTempDir' "does not report the root pom or any submodule as a dependency" $ \dir -> do
createMultiModuleFixture dir
onStaticGraph dir allTargets $ \graph ->
vertexNames graph `shouldBe'` ["com.google.guava:guava", "junit:junit", "org.apache.commons:commons-lang3"]

-- Submodule filtering deletes the root pom's node, which is the only direct
-- node in the graph the pom tactic builds. Unless the surviving submodules are
-- promoted to direct before being removed, this reports nothing as direct.
itWithTempDir' "reports the selected submodule's dependencies as direct" $ \dir -> do
createMultiModuleFixture dir
onStaticGraph dir (onlyTargets ["com.example:mod-a"]) $ \graph ->
directNames graph `shouldBe'` ["junit:junit", "org.apache.commons:commons-lang3"]

directNames :: Graphing Dependency -> [Text]
directNames = sort . map dependencyName . Graphing.directList

vertexNames :: Graphing Dependency -> [Text]
vertexNames = sort . map dependencyName . Graphing.vertexList

-- | Statically analyze the maven project in @dir@ and hand its dependency graph
-- to @act@. @select@ picks which build targets to analyze.
onStaticGraph ::
Path Abs Dir ->
(MavenProject -> FoundTargets) ->
(Graphing Dependency -> EffectStack ()) ->
EffectStack ()
onStaticGraph dir select act = do
closures <- findProjects dir
case closures of
[closure] -> do
let project = MavenProject closure
results <- runReader noScopeFilters $ getDepsStatically (select project) project
act $ dependencyGraph results
-- Each fixture is one project closure; a different count means discovery went
-- wrong, and saying so is more useful than an assertion about the graph.
_ -> expectationFailure' $ "expected one project closure, found " <> show (length closures)
where
noScopeFilters :: MavenScopeFilters
noScopeFilters = MavenScopeIncludeFilters mempty

-- | Every submodule, as @fossa analyze@ selects them when given no target filter.
allTargets :: MavenProject -> FoundTargets
allTargets = projectBuildTargets . mkProject

-- | Only the named submodules.
onlyTargets :: [Text] -> MavenProject -> FoundTargets
onlyTargets targets _ =
maybe ProjectWithoutTargets FoundTargets . nonEmpty . Set.fromList $ map BuildTarget targets

writePom :: Path Abs Dir -> Path Rel File -> BS.ByteString -> EffectStack ()
writePom dir name = sendIO . BS.writeFile (toFilePath (dir </> name))

-- | Writes a two-module project into @dir@:
--
-- @
-- pom.xml -- com.example:root:1.0, packaging=pom, declares commons-lang3
-- mod-a/pom.xml -- declares junit
-- mod-b/pom.xml -- declares guava
-- @
createMultiModuleFixture :: Path Abs Dir -> EffectStack ()
createMultiModuleFixture dir = do
sendIO $ PIO.createDirIfMissing True (dir </> [reldir|mod-a|])
sendIO $ PIO.createDirIfMissing True (dir </> [reldir|mod-b|])
writePom dir [relfile|pom.xml|] multiModuleRootPom
writePom (dir </> [reldir|mod-a|]) [relfile|pom.xml|] (modulePom "mod-a" "junit" "junit" "4.13.2")
writePom (dir </> [reldir|mod-b|]) [relfile|pom.xml|] (modulePom "mod-b" "com.google.guava" "guava" "31.1-jre")

-- | Concatenates lines of an XML document.
packLines :: [String] -> BS.ByteString
packLines = BS.concat . map BS.pack

singleModulePom :: BS.ByteString
singleModulePom =
packLines
[ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
, "<project xmlns=\"http://maven.apache.org/POM/4.0.0\">\n"
, " <modelVersion>4.0.0</modelVersion>\n"
, " <groupId>com.example</groupId>\n"
, " <artifactId>demo</artifactId>\n"
, " <version>1.0</version>\n"
, " <dependencies>\n"
, " <dependency>\n"
, " <groupId>junit</groupId>\n"
, " <artifactId>junit</artifactId>\n"
, " <version>4.13.2</version>\n"
, " </dependency>\n"
, " <dependency>\n"
, " <groupId>org.apache.commons</groupId>\n"
, " <artifactId>commons-lang3</artifactId>\n"
, " <version>3.12.0</version>\n"
, " </dependency>\n"
, " </dependencies>\n"
, "</project>\n"
]

multiModuleRootPom :: BS.ByteString
multiModuleRootPom =
packLines
[ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
, "<project xmlns=\"http://maven.apache.org/POM/4.0.0\">\n"
, " <modelVersion>4.0.0</modelVersion>\n"
, " <groupId>com.example</groupId>\n"
, " <artifactId>root</artifactId>\n"
, " <version>1.0</version>\n"
, " <packaging>pom</packaging>\n"
, " <modules>\n"
, " <module>mod-a</module>\n"
, " <module>mod-b</module>\n"
, " </modules>\n"
, " <dependencies>\n"
, " <dependency>\n"
, " <groupId>org.apache.commons</groupId>\n"
, " <artifactId>commons-lang3</artifactId>\n"
, " <version>3.12.0</version>\n"
, " </dependency>\n"
, " </dependencies>\n"
, "</project>\n"
]

modulePom :: String -> String -> String -> String -> BS.ByteString
modulePom artifactId depGroup depArtifact depVersion' =
packLines
[ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
, "<project xmlns=\"http://maven.apache.org/POM/4.0.0\">\n"
, " <modelVersion>4.0.0</modelVersion>\n"
, " <parent>\n"
, " <groupId>com.example</groupId>\n"
, " <artifactId>root</artifactId>\n"
, " <version>1.0</version>\n"
, " <relativePath>../pom.xml</relativePath>\n"
, " </parent>\n"
, " <artifactId>" <> artifactId <> "</artifactId>\n"
, " <dependencies>\n"
, " <dependency>\n"
, " <groupId>" <> depGroup <> "</groupId>\n"
, " <artifactId>" <> depArtifact <> "</artifactId>\n"
, " <version>" <> depVersion' <> "</version>\n"
, " </dependency>\n"
, " </dependencies>\n"
, "</project>\n"
]
Loading