Skip to content

Commit 46374e2

Browse files
authored
Make pipenv strategy differentiate between direct and transitive deps (#1601)
* Fix pipenv strategy reporting all deps as direct * Tests * Fix broken swift links * Get pipenv direct deps from Pipfile * Fix tests * Fix lint error * Fix integration test * Revert incorrect doc change * Add Pipfile as manifest file * Fix test * Fix outdated comment * Code review comments * Fix missing transitive deps in static analysis * Expand comment about pruning
1 parent ab207a2 commit 46374e2

5 files changed

Lines changed: 93 additions & 46 deletions

File tree

docs/references/strategies/platforms/ios/swift.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,5 @@ For swift, we consider the Xcode project to be a valid Xcode project, if and onl
163163

164164
## References
165165

166-
- [Swift Package Manager](https://github.com/apple/swift-package-manager)
167-
- [Package.swift, must begin with version specifier](https://github.com/apple/swift-package-manager/blob/main/Documentation/PackageDescription.md#about-the-swift-tools-version)
168-
- [Package.swift, must be defined in a single nested statement, and should not be modified after initialization](https://github.com/apple/swift-package-manager/blob/main/Documentation/PackageDescription.md#package)
166+
- [Swift Package Manager](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs)
167+
- [Package.swift documentation](https://docs.swift.org/swiftpm/documentation/packagedescription/package/)

integration-test/Analysis/Python/PipenvSpec.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ pipenv =
2525

2626
spec :: Spec
2727
spec = do
28-
testSuiteDepResultSummary NonStrict pipenv PipenvProjectType (DependencyResultsSummary 90 90 0 1 Complete)
28+
testSuiteDepResultSummary NonStrict pipenv PipenvProjectType (DependencyResultsSummary 90 6 0 2 Complete)

src/App/Fossa/Analyze/Project.hs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ mkResult basedir project pathPrefix dependencyResults =
1818
, projectResultPath = projectPath project
1919
, projectResultGraph =
2020
-- FIXME: this is a hack to work around analyzers that aren't able to
21-
-- determine which dependencies are direct. Without this hack, all of
21+
-- determine which dependencies are direct or have no edges. Without this hack, all of
2222
-- their dependencies would be filtered out. The real fix to this is to
2323
-- have a separate designation for "reachable" vs "direct" on nodes in a
2424
-- Graphing, where direct deps are inherently reachable.
25-
if null (Graphing.directList graph) || shouldKeepUnreachableDeps (projectType project)
25+
if null (Graphing.directList graph)
26+
|| null (Graphing.edgesList graph)
27+
|| shouldKeepUnreachableDeps (projectType project)
2628
then graph
2729
else Graphing.pruneUnreachable graph
2830
, projectResultGraphBreadth = dependencyGraphBreadth dependencyResults

src/Strategy/Python/Pipenv.hs

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ module Strategy.Python.Pipenv (
77
mkProject,
88
PipenvGraphDep (..),
99
PipfileLock (..),
10+
PipfileToml (..),
1011
PipfileMeta (..),
1112
PipfileSource (..),
1213
PipfileDep (..),
1314
PipenvProject (..),
1415
buildGraph,
16+
pipfilePackageList,
1517
) where
1618

1719
import App.Fossa.Analyze.Types (AnalyzeProject (analyzeProjectStaticOnly), analyzeProject)
@@ -26,16 +28,19 @@ import Control.Effect.Diagnostics (
2628
warnOnErr,
2729
)
2830
import Control.Effect.Reader (Reader)
31+
import Control.Monad (join)
2932
import Data.Aeson (
3033
FromJSON (parseJSON),
3134
ToJSON,
3235
withObject,
3336
(.:),
3437
(.:?),
3538
)
39+
import Data.Bifunctor (bimap)
3640
import Data.Foldable (for_, traverse_)
3741
import Data.Map.Strict (Map)
3842
import Data.Map.Strict qualified as Map
43+
import Data.Maybe (isJust)
3944
import Data.Set (Set)
4045
import Data.Text (Text)
4146
import Data.Text qualified as Text
@@ -61,16 +66,18 @@ import Discovery.Walk (
6166
import Effect.Exec (AllowErr (Never), Command (..), Exec, execJson)
6267
import Effect.Grapher (
6368
LabeledGrapher,
69+
deep,
6470
direct,
6571
edge,
6672
label,
6773
withLabeling,
6874
)
69-
import Effect.ReadFS (ReadFS, readContentsJson)
75+
import Effect.ReadFS (ReadFS, readContentsJson, readContentsToml)
7076
import GHC.Generics (Generic)
71-
import Graphing (Graphing)
77+
import Graphing (Graphing, pruneUnreachable)
7278
import Path (Abs, Dir, File, Path, parent)
7379
import Strategy.Python.Errors (PipenvCmdFailed (..))
80+
import Toml.Schema qualified
7481
import Types (
7582
DependencyResults (..),
7683
DiscoveredProject (..),
@@ -83,9 +90,12 @@ discover = simpleDiscover findProjects mkProject PipenvProjectType
8390

8491
findProjects :: (Has ReadFS sig m, Has Diagnostics sig m, Has (Reader AllFilters) sig m) => Path Abs Dir -> m [PipenvProject]
8592
findProjects = walkWithFilters' $ \_ _ files -> do
86-
case findFileNamed "Pipfile.lock" files of
87-
Nothing -> pure ([], WalkContinue)
88-
Just file -> pure ([PipenvProject file], WalkContinue)
93+
case findPipenvFiles files of
94+
(Nothing, _) -> pure ([], WalkContinue)
95+
(_, Nothing) -> pure ([], WalkContinue)
96+
(Just pipfile, Just lock) -> pure ([PipenvProject pipfile lock], WalkContinue)
97+
where
98+
findPipenvFiles files = join bimap (`findFileNamed` files) ("Pipfile", "Pipfile.lock")
8999

90100
getDeps ::
91101
( Has ReadFS sig m
@@ -95,7 +105,8 @@ getDeps ::
95105
PipenvProject ->
96106
m DependencyResults
97107
getDeps project = context "Pipenv" $ do
98-
lock <- context "Getting direct dependencies" $ readContentsJson (pipenvLockfile project)
108+
lock <- context "Getting dependencies from Pipfile.lock" $ readContentsJson (pipenvLockfile project)
109+
pipfile <- context "Getting dependencies from Pipfile" $ readContentsToml (pipenvPipfile project)
99110

100111
maybeDeps <-
101112
context "Getting deep dependencies"
@@ -106,12 +117,12 @@ getDeps project = context "Pipenv" $ do
106117
. errHelp PipenvCmdFailedHelp
107118
$ execJson (parent (pipenvLockfile project)) pipenvGraphCmd
108119

109-
graph <- context "Building dependency graph" $ pure (buildGraph lock maybeDeps)
120+
let graph = buildGraph pipfile lock maybeDeps
110121
pure $
111122
DependencyResults
112123
{ dependencyGraph = graph
113124
, dependencyGraphBreadth = Complete
114-
, dependencyManifestFiles = [pipenvLockfile project]
125+
, dependencyManifestFiles = [pipenvLockfile project, pipenvPipfile project]
115126
}
116127

117128
getDepsStatically ::
@@ -121,8 +132,9 @@ getDepsStatically ::
121132
PipenvProject ->
122133
m DependencyResults
123134
getDepsStatically project = context "Pipenv" $ do
124-
lock <- context "Getting direct dependencies" $ readContentsJson (pipenvLockfile project)
125-
graph <- context "Building dependency graph" $ pure (buildGraph lock Nothing)
135+
lock <- context "Getting dependencies from Pipfile.lock" $ readContentsJson (pipenvLockfile project)
136+
pipfile <- context "Getting dependencies from Pipfile" $ readContentsToml (pipenvPipfile project)
137+
let graph = buildGraph pipfile lock Nothing
126138
pure $
127139
DependencyResults
128140
{ dependencyGraph = graph
@@ -139,8 +151,9 @@ mkProject project =
139151
, projectData = project
140152
}
141153

142-
newtype PipenvProject = PipenvProject
143-
{ pipenvLockfile :: Path Abs File
154+
data PipenvProject = PipenvProject
155+
{ pipenvPipfile :: Path Abs File
156+
, pipenvLockfile :: Path Abs File
144157
}
145158
deriving (Eq, Ord, Show, Generic)
146159

@@ -158,9 +171,9 @@ pipenvGraphCmd =
158171
, cmdAllowErr = Never
159172
}
160173

161-
buildGraph :: PipfileLock -> Maybe [PipenvGraphDep] -> Graphing Dependency
162-
buildGraph lock maybeDeps = run . withLabeling toDependency $ do
163-
buildNodes lock
174+
buildGraph :: PipfileToml -> PipfileLock -> Maybe [PipenvGraphDep] -> Graphing Dependency
175+
buildGraph pipfile lock maybeDeps = prune $ run . withLabeling toDependency $ do
176+
buildNodes pipfile lock
164177
traverse_ buildEdges maybeDeps
165178
where
166179
toDependency :: PipPkg -> Set PipLabel -> Dependency
@@ -180,6 +193,13 @@ buildGraph lock maybeDeps = run . withLabeling toDependency $ do
180193
, dependencyTags = Map.empty
181194
}
182195

196+
-- We only have edges if we have graph dependencies. If running with --static-only-analysis
197+
-- then there will be no graph dependencies, so this situation is not uncommon.
198+
-- If we have no graph dependencies, then we have no edges, so calling `pruneUnreachable`
199+
-- would result in all but the direct dependencies getting removed, and so we need to skip
200+
-- pruning in this case.
201+
prune = if isJust maybeDeps then pruneUnreachable else id
202+
183203
data PipPkg = PipPkg
184204
{ pipPkgName :: Text
185205
, pipPkgVersion :: Maybe Text
@@ -193,8 +213,8 @@ data PipLabel
193213
| PipEnvironment DepEnvironment
194214
deriving (Eq, Ord, Show)
195215

196-
buildNodes :: forall sig m. Has PipGrapher sig m => PipfileLock -> m ()
197-
buildNodes PipfileLock{..} = do
216+
buildNodes :: forall sig m. Has PipGrapher sig m => PipfileToml -> PipfileLock -> m ()
217+
buildNodes PipfileToml{..} PipfileLock{..} = do
198218
let indexBy :: Ord k => (v -> k) -> [v] -> Map k v
199219
indexBy ix = Map.fromList . map (\v -> (ix v, v))
200220

@@ -210,10 +230,11 @@ buildNodes PipfileLock{..} = do
210230
Text -> -- dep name
211231
PipfileDep ->
212232
m ()
213-
addWithEnv env sourcesMap depName dep = do
214-
let pkg = PipPkg depName (Text.drop 2 <$> fileDepVersion dep)
215-
-- TODO: reachable instead of direct
216-
direct pkg
233+
addWithEnv env sourcesMap name dep = do
234+
let pkg = PipPkg name (Text.drop 2 <$> fileDepVersion dep)
235+
-- Use the Pipfile as the source of truth for which dependencies are direct
236+
let graphFn = if Map.member name pipfilePackages || Map.member name pipfileDevPackages then direct else deep
237+
graphFn pkg
217238
label pkg (PipEnvironment env)
218239

219240
-- add label for source when it exists
@@ -302,3 +323,26 @@ instance FromJSON PipenvGraphDep where
302323
<*> obj .: "installed_version"
303324
<*> obj .: "required_version"
304325
<*> obj .: "dependencies"
326+
327+
---------- Pipfile
328+
329+
data PipfileToml = PipfileToml
330+
{ pipfilePackages :: Map Text PipfilePackageVersion
331+
, pipfileDevPackages :: Map Text PipfilePackageVersion
332+
}
333+
deriving (Eq, Show)
334+
335+
instance Toml.Schema.FromValue PipfileToml where
336+
fromValue =
337+
Toml.Schema.parseTableFromValue $
338+
PipfileToml
339+
<$> Toml.Schema.pickKey [Toml.Schema.Key "packages" Toml.Schema.fromValue, Toml.Schema.Else (pure mempty)]
340+
<*> Toml.Schema.pickKey [Toml.Schema.Key "dev-packages" Toml.Schema.fromValue, Toml.Schema.Else (pure mempty)]
341+
342+
-- We don't need to parse the package versions in the Pipfile as we get version info from the lock file
343+
data PipfilePackageVersion = PipfilePackageVersion deriving (Eq, Ord, Show)
344+
instance Toml.Schema.FromValue PipfilePackageVersion where
345+
fromValue _ = pure PipfilePackageVersion
346+
347+
pipfilePackageList :: [Text] -> Map Text PipfilePackageVersion
348+
pipfilePackageList = Map.fromList . map (,PipfilePackageVersion)

test/Python/PipenvSpec.hs

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@ pipfileLock =
5757
]
5858
}
5959

60+
pipfileToml :: PipfileToml
61+
pipfileToml =
62+
PipfileToml
63+
(pipfilePackageList ["pkgOne"])
64+
(pipfilePackageList ["pkgTwo"])
65+
6066
pipenvOutput :: [PipenvGraphDep]
6167
pipenvOutput =
6268
[ PipenvGraphDep
63-
{ depName = "pkgOne"
64-
, depInstalled = "1.0.0"
65-
, depRequired = "==1.0.0"
66-
, depDependencies = []
67-
}
68-
, PipenvGraphDep
6969
{ depName = "pkgTwo"
7070
, depInstalled = "2.0.0"
7171
, depRequired = "==2.0.0"
@@ -76,6 +76,12 @@ pipenvOutput =
7676
, depRequired = "==3.0.0"
7777
, depDependencies = []
7878
}
79+
, PipenvGraphDep
80+
{ depName = "pkgOne"
81+
, depInstalled = "1.0.0"
82+
, depRequired = "==1.0.0"
83+
, depDependencies = []
84+
}
7985
]
8086
}
8187
]
@@ -124,36 +130,32 @@ depFour =
124130
, dependencyTags = Map.empty
125131
}
126132

127-
xit :: String -> Expectation -> SpecWith (Arg Expectation)
128-
xit _ _ = it "is an ignored test" $ () `shouldBe` ()
129-
130133
spec :: Spec
131134
spec = do
132135
pipLockFile <- runIO (BS.readFile "test/Python/testdata/Pipfile.lock")
133136

134137
describe "analyzeWithCmd" $
135-
-- FIXME: graphing needs to be refactored to include "reachable" alongside "direct"
136-
xit "should use pipenv output for edges and tags" $ do
137-
let result = buildGraph pipfileLock (Just pipenvOutput)
138+
it "should use pipenv output for edges and tags" $ do
139+
let result = buildGraph pipfileToml pipfileLock (Just pipenvOutput)
138140

139141
expectDeps [depOne, depTwo, depThree] result
140142
expectDirect [depOne, depTwo] result
141-
expectEdges [(depTwo, depThree)] result
143+
expectEdges [(depTwo, depThree), (depTwo, depOne)] result
142144

143145
describe "analyzeNoCmd" $
144-
it "should set all dependencies as direct" $ do
145-
let result = buildGraph pipfileLock Nothing
146+
it "should have no edges and unreachable deps" $ do
147+
let result = buildGraph pipfileToml pipfileLock Nothing
146148

147149
expectDeps [depOne, depTwo, depThree, depFour] result
148-
expectDirect [depOne, depTwo, depThree, depFour] result
150+
expectDirect [depOne, depTwo] result
149151
expectEdges [] result
150152

151153
describe "analyzeNoCmdFromFile" $
152-
it "should set all dependencies as direct" $ do
154+
it "should have no edges and unreachable deps" $ do
153155
case eitherDecodeStrict pipLockFile of
154156
Right res -> do
155-
let result = buildGraph res Nothing
157+
let result = buildGraph pipfileToml res Nothing
156158
expectDeps [depOne, depTwo, depThree, depFour] result
157-
expectDirect [depOne, depTwo, depThree, depFour] result
159+
expectDirect [depOne, depTwo] result
158160
expectEdges [] result
159161
Left _ -> expectationFailure "failed to parse"

0 commit comments

Comments
 (0)