@@ -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
1719import App.Fossa.Analyze.Types (AnalyzeProject (analyzeProjectStaticOnly ), analyzeProject )
@@ -26,16 +28,19 @@ import Control.Effect.Diagnostics (
2628 warnOnErr ,
2729 )
2830import Control.Effect.Reader (Reader )
31+ import Control.Monad (join )
2932import Data.Aeson (
3033 FromJSON (parseJSON ),
3134 ToJSON ,
3235 withObject ,
3336 (.:) ,
3437 (.:?) ,
3538 )
39+ import Data.Bifunctor (bimap )
3640import Data.Foldable (for_ , traverse_ )
3741import Data.Map.Strict (Map )
3842import Data.Map.Strict qualified as Map
43+ import Data.Maybe (isJust )
3944import Data.Set (Set )
4045import Data.Text (Text )
4146import Data.Text qualified as Text
@@ -61,16 +66,18 @@ import Discovery.Walk (
6166import Effect.Exec (AllowErr (Never ), Command (.. ), Exec , execJson )
6267import 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 )
7076import GHC.Generics (Generic )
71- import Graphing (Graphing )
77+ import Graphing (Graphing , pruneUnreachable )
7278import Path (Abs , Dir , File , Path , parent )
7379import Strategy.Python.Errors (PipenvCmdFailed (.. ))
80+ import Toml.Schema qualified
7481import Types (
7582 DependencyResults (.. ),
7683 DiscoveredProject (.. ),
@@ -83,9 +90,12 @@ discover = simpleDiscover findProjects mkProject PipenvProjectType
8390
8491findProjects :: (Has ReadFS sig m , Has Diagnostics sig m , Has (Reader AllFilters ) sig m ) => Path Abs Dir -> m [PipenvProject ]
8592findProjects = 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
90100getDeps ::
91101 ( Has ReadFS sig m
@@ -95,7 +105,8 @@ getDeps ::
95105 PipenvProject ->
96106 m DependencyResults
97107getDeps 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
117128getDepsStatically ::
@@ -121,8 +132,9 @@ getDepsStatically ::
121132 PipenvProject ->
122133 m DependencyResults
123134getDepsStatically 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+
183203data 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 )
0 commit comments