diff --git a/Changelog.md b/Changelog.md
index 0bcfd7fb5..84fc6258a 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,7 +1,8 @@
# Spectrometer Changelog
-# unreleased
+## v2.15.0
+- Dart: Adds support for pub package manager. ([#313](https://github.com/fossas/spectrometer/pull/313))
- Modified DiscoveredProject's to include manifest file information (origin paths) ([#316](https://github.com/fossas/spectrometer/pull/316))
## v2.14.5
diff --git a/docs/quickreference/pub.md b/docs/quickreference/pub.md
new file mode 100644
index 000000000..3aa7e66ce
--- /dev/null
+++ b/docs/quickreference/pub.md
@@ -0,0 +1,12 @@
+# Quick reference: pub
+
+## Requirements
+
+**Ideal/Minimum**
+- `dart` installed locally
+- `pubspec.yaml` file present in your project
+- `pubspec.lock` file present in your project, with dependencies already retrieved and resolved
+
+## Project discovery
+
+Directories containing `pubspec.yaml` files are considered dart projects.
\ No newline at end of file
diff --git a/docs/strategies.md b/docs/strategies.md
index 1efaecba0..bd541d7ca 100644
--- a/docs/strategies.md
+++ b/docs/strategies.md
@@ -70,6 +70,7 @@ TODO: create a lookup table that categorizes these strategies by language or too
The CLI supports the following strategies:
- [clojure](strategies/golang.md) (lein)
+- [dart](strategies/dart.md) (pub)
- [elixir](strategies/elixir.md) (mix)
- [erlang](strategies/erlang.md) (rebar3)
- [golang](strategies/golang.md) (gomodules, dep, glide)
diff --git a/docs/strategies/dart-resolved-graph-with-lock-cmd.svg b/docs/strategies/dart-resolved-graph-with-lock-cmd.svg
new file mode 100644
index 000000000..c5e3eb851
--- /dev/null
+++ b/docs/strategies/dart-resolved-graph-with-lock-cmd.svg
@@ -0,0 +1,327 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/strategies/dart-resolved-graph-without-cmd.svg b/docs/strategies/dart-resolved-graph-without-cmd.svg
new file mode 100644
index 000000000..32ed7c202
--- /dev/null
+++ b/docs/strategies/dart-resolved-graph-without-cmd.svg
@@ -0,0 +1,39 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/strategies/dart.md b/docs/strategies/dart.md
new file mode 100644
index 000000000..60f704d31
--- /dev/null
+++ b/docs/strategies/dart.md
@@ -0,0 +1,278 @@
+# Dart
+
+Dart ecosystem uses pub package manager to manage shared [packages and libraries](https://dart.dev/guides/packages). Packages can be sourced from [registry](https://pub.dev/), [git repository](https://dart.dev/tools/pub/dependencies#git-packages), or from [local file system](https://dart.dev/tools/pub/dependencies#path-packages).
+
+## Project Discovery
+
+Find file named `pubspec.yaml`.
+
+## Analysis
+
+We attempt to perform all of the strategies below, we select the result of succeeded strategies which has the highest preference.
+
+| Preference | Strategy | Direct Deps | Deep Deps | Edges |
+| ---------- | ------------------------------------------------------------------------------------------------------ | ------------------ | ------------------ | ------------------ |
+| Highest | 1. `pubspec.yaml` and `pubspec.lock` are discovered, and `flutter pub deps -s compact` can be executed | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
+| | 2. `pubspec.yaml` and `pubspec.lock` are discovered, and `dart pub deps -s compact` can be executed | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
+| | 3. `pubspec.yaml` and `pubspec.lock` are discovered, and `pub deps -s compact` can be executed | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
+| | 4. `pubspec.yaml` and `pubspec.lock` are discovered | :heavy_check_mark: | :x: | :x: |
+| Lowest | 5. Only `pubspec.yaml` is discovered | :heavy_check_mark: | :x: | :x: |
+
+Where,
+
+* :heavy_check_mark: - Supported in all projects
+* :x: - Not Supported
+
+It is recommended that, `pub deps get` is executed prior to analyzing dart project. This ensures dependencies are retrieved, so `pub deps -s compact` command can produce edges between direct, and deep dependencies.
+
+### Limitations
+
+* [Path dependencies](https://dart.dev/tools/pub/dependencies#path-packages) are not reported, and will be ignored in analyses. All descendant dependencies of the path dependency will be promoted to the ancestor of the path dependency.
+* [Sdk dependencies](https://dart.dev/tools/pub/dependencies#sdk) are not reported, and will be ignored in analyses. All descendant dependencies of the sdk dependency will be promoted to the ancestor of the sdk dependency.
+
+# Example
+
+Create new dart project by creating `pubspec.yaml` file.
+
+```yaml
+name: some_example
+description: some example description
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.0.0 <3.0.0"
+
+dependencies:
+ path: ">= 1.2.0 <3.0.0"
+ encrypt:
+ git: https://github.com/leocavalcante/encrypt.git
+ flutter:
+ sdk: flutter
+ provider: ^5.0.0
+ quiver: any
+
+flutter:
+ uses-material-design: true
+```
+
+Execute `dart pub get` to retrieve packages from the spec file. When performed, it will create `pubspec.lock` file.
+
+```text
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ args:
+ dependency: transitive
+ description:
+ name: args
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.2.0"
+ asn1lib:
+ dependency: transitive
+ description:
+ name: asn1lib
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.2"
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ clock:
+ dependency: transitive
+ description:
+ name: clock
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.15.0"
+ convert:
+ dependency: transitive
+ description:
+ name: convert
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ crypto:
+ dependency: transitive
+ description:
+ name: crypto
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ encrypt:
+ dependency: "direct main"
+ description:
+ path: "."
+ ref: HEAD
+ resolved-ref: bc2a3f44339574edb5c374b991b6386c495a1bbb
+ url: "https://github.com/leocavalcante/encrypt.git"
+ source: git
+ version: "5.0.1"
+ flutter:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ js:
+ dependency: transitive
+ description:
+ name: js
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.6.3"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.12.11"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.7.0"
+ nested:
+ dependency: transitive
+ description:
+ name: nested
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0"
+ path:
+ dependency: "direct main"
+ description:
+ name: path
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.8.0"
+ pointycastle:
+ dependency: transitive
+ description:
+ name: pointycastle
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.3.0"
+ provider:
+ dependency: "direct main"
+ description:
+ name: provider
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "5.0.0"
+ quiver:
+ dependency: "direct main"
+ description:
+ name: quiver
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ sky_engine:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.99"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.10.0"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.0"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+sdks:
+ dart: ">=2.12.0 <3.0.0"
+ flutter: ">=1.16.0"
+```
+
+Dependencies can be inspected using `dart pub deps -s compact`:
+
+```text
+Dart SDK 2.14.0-301.0.dev
+Flutter SDK 2.4.0-4.0.pre
+some_example 1.0.0+1
+
+dependencies:
+- encrypt 5.0.1 [args asn1lib clock collection crypto pointycastle]
+- flutter 0.0.0 [characters collection meta typed_data vector_math sky_engine]
+- path 1.8.0
+- provider 5.0.0 [collection flutter nested]
+- quiver 3.0.1 [matcher]
+
+transitive dependencies:
+- args 2.2.0
+- asn1lib 1.0.2
+- characters 1.1.0
+- clock 1.1.0
+- collection 1.15.0
+- convert 3.0.1 [typed_data]
+- crypto 3.0.1 [collection typed_data]
+- js 0.6.3
+- matcher 0.12.11 [stack_trace]
+- meta 1.7.0
+- nested 1.0.0 [flutter]
+- pointycastle 3.3.0 [collection convert js]
+- sky_engine 0.0.99
+- stack_trace 1.10.0 [path]
+- typed_data 1.3.0 [collection]
+- vector_math 2.1.0
+```
+
+When pub deps command is successfully executed, and lockfile id discovered (strategy 1, 2, or 3) analyses would yield following dependency graph:
+
+![With lock file and deps command](dart-resolved-graph-with-lock-cmd.svg)
+
+Note: Dependencies in yellow boxes are direct dependencies, rest are deep dependencies. All descendent dependencies of sdk dependencies are promoted to their ancestor - e.g. characters, collection, meta, typed_data, and vector_math.
+
+If pub deps command is not successfully executed:
+
+![Without deps command](dart-resolved-graph-without-cmd.svg)
+
+## FAQ
+
+### How do I *only* analyze dart projects?
+
+You can explicitly specify analyses target in `.fossa.yml` file.
+
+Example below, will exclude all analyses targets except pub.
+
+```yaml
+# .fossa.yml
+
+version: 3
+targets:
+ only:
+ - type: pub
+```
+
+## References
+
+* [Pub](https://dart.dev/tools/pub/cmd)
+* [Dart](https://dart.dev/tools/pub/cmd)
+* [Flutter](https://flutter.dev/)
diff --git a/docs/userguide.md b/docs/userguide.md
index 3e05bea46..52be1434e 100644
--- a/docs/userguide.md
+++ b/docs/userguide.md
@@ -9,6 +9,7 @@
- [Check for FOSSA scan results](#check-for-fossa-scan-results)
- [Supported Languages](#supported-languages)
- [clojure](#clojure)
+ - [dart](#dart)
- [erlang](#erlang)
- [golang](#golang)
- [haskell](#haskell)
@@ -85,6 +86,10 @@ fossa analyze --help
- [leiningen](quickreference/leiningen.md)
+### dart
+
+- [pub](quickreference/pub.md)
+
### erlang
- [rebar3](quickreference/rebar3.md)
@@ -249,6 +254,7 @@ Supported dependency types:
- `maven` - Maven dependencies that can be found at many different sources. Specified as `name: javax.xml.bind:jaxb-api` where the convention is `groupId:artifactId`.
- `npm` - Javascript dependencies found at [npmjs.com](https://www.npmjs.com/).
- `nuget` - .NET dependencies found at [NuGet.org](https://www.nuget.org/).
+- `pub` - Dart dependencies found at [pub.dev](https://www.pub.dev/).
- `pypi` - Python dependencies that are typically found at [Pypi.org](https://pypi.org/).
- `cocoapods` - Swift and Objective-C dependencies found at [Cocoapods.org](https://cocoapods.org/).
- `url` - The URL type allows you to specify only the download location of an archive (e.g.: `.zip`, .`tar.gz`, etc.) in the `name` field and the FOSSA backend will attempt to download and scan it. Example for a github source dependency `https://github.com/fossas/spectrometer/archive/refs/tags/v2.7.2.tar.gz`. The `version` field will be silently ignored for `url` type dependencies.
diff --git a/spectrometer.cabal b/spectrometer.cabal
index ac4f89ab5..ee166e524 100644
--- a/spectrometer.cabal
+++ b/spectrometer.cabal
@@ -227,6 +227,9 @@ library
Strategy.Conda
Strategy.Conda.CondaList
Strategy.Conda.EnvironmentYml
+ Strategy.Dart.PubDeps
+ Strategy.Dart.PubSpec
+ Strategy.Dart.PubSpecLock
Strategy.Elixir.MixTree
Strategy.Erlang.ConfigParser
Strategy.Erlang.Rebar3Tree
@@ -264,6 +267,7 @@ library
Strategy.NuGet.Paket
Strategy.NuGet.ProjectAssetsJson
Strategy.NuGet.ProjectJson
+ Strategy.Pub
Strategy.Python.Pipenv
Strategy.Python.Poetry
Strategy.Python.Poetry.Common
@@ -327,6 +331,9 @@ test-suite unit-tests
Conda.CondaListSpec
Conda.EnvironmentYmlSpec
Control.Carrier.DiagnosticsSpec
+ Dart.PubDepsSpec
+ Dart.PubSpecSpec
+ Dart.PubSpecLockSpec
Discovery.FiltersSpec
Effect.ExecSpec
Elixir.MixTreeSpec
diff --git a/src/App/Fossa/Analyze.hs b/src/App/Fossa/Analyze.hs
index 952e84ede..ac2534877 100644
--- a/src/App/Fossa/Analyze.hs
+++ b/src/App/Fossa/Analyze.hs
@@ -84,6 +84,7 @@ import Strategy.NuGet.PackagesConfig qualified as PackagesConfig
import Strategy.NuGet.Paket qualified as Paket
import Strategy.NuGet.ProjectAssetsJson qualified as ProjectAssetsJson
import Strategy.NuGet.ProjectJson qualified as ProjectJson
+import Strategy.Pub qualified as Pub
import Strategy.Python.Pipenv qualified as Pipenv
import Strategy.Python.Poetry qualified as Poetry
import Strategy.Python.Setuptools qualified as Setuptools
@@ -192,6 +193,7 @@ discoverFuncs =
, Poetry.discover
, ProjectAssetsJson.discover
, ProjectJson.discover
+ , Pub.discover
, RPM.discover
, Rebar3.discover
, RepoManifest.discover
diff --git a/src/DepTypes.hs b/src/DepTypes.hs
index e17c19a76..e28eab561 100644
--- a/src/DepTypes.hs
+++ b/src/DepTypes.hs
@@ -76,6 +76,8 @@ data DepType
PipType
| -- | Cocoapods registry
PodType
+ | -- | Pub dependency for dart
+ PubType
| -- | Go dependency
GoType
| -- | Rust Cargo Dependency
diff --git a/src/Srclib/Converter.hs b/src/Srclib/Converter.hs
index 46acb6cd1..a2e788471 100644
--- a/src/Srclib/Converter.hs
+++ b/src/Srclib/Converter.hs
@@ -118,3 +118,4 @@ depTypeToFetcher = \case
SubprojectType -> "mvn" -- FIXME. I knew SubprojectType would come back to bite us.
URLType -> "url"
UserType -> "user"
+ PubType -> "pub"
diff --git a/src/Strategy/Dart/PubDeps.hs b/src/Strategy/Dart/PubDeps.hs
new file mode 100644
index 000000000..065cc820e
--- /dev/null
+++ b/src/Strategy/Dart/PubDeps.hs
@@ -0,0 +1,203 @@
+module Strategy.Dart.PubDeps (
+ analyzeDepsCmd,
+
+ -- * for testing
+ dartPubDepCmd,
+ depsCmdOutputParser,
+ flutterPubDepCmd,
+ buildGraph,
+ PubDepPackage (..),
+) where
+
+import Control.Effect.Diagnostics (Diagnostics, context, (<||>))
+import Data.List (find)
+import Data.Map.Strict qualified as Map
+import Data.Maybe (fromMaybe)
+import Data.Set (Set, toList)
+import Data.Set qualified as Set
+import Data.String.Conversion (ToText (toText))
+import Data.Text (Text)
+import Data.Void (Void)
+import DepTypes (Dependency (..))
+import Effect.Exec (AllowErr (Never), Command (..), Exec, execParser)
+import Effect.Logger (Logger (..))
+import Effect.ReadFS (Has, ReadFS, readContentsYaml)
+import GHC.Generics (Generic)
+import Graphing (Graphing, deeps, directs, edges, gmap, shrink)
+import Path
+import Strategy.Dart.PubSpecLock (
+ PackageName (..),
+ PubDepSource (..),
+ PubLockContent (..),
+ PubLockPackageHostedSource (..),
+ PubLockPackageMetadata (..),
+ isSupported,
+ logIgnoredPackages,
+ toDependency,
+ )
+import Text.Megaparsec (
+ MonadParsec (eof),
+ Parsec,
+ anySingle,
+ between,
+ many,
+ optional,
+ sepBy,
+ skipManyTill,
+ (<|>),
+ )
+import Text.Megaparsec qualified as Megaparsec
+import Text.Megaparsec.Char (alphaNumChar, char, space1)
+import Text.Megaparsec.Char.Lexer qualified as L
+import Types (GraphBreadth (..))
+
+type Parser = Parsec Void Text
+
+symbol :: Text -> Parser Text
+symbol = L.symbol scn
+
+scn :: Parser ()
+scn = L.space space1 Megaparsec.empty Megaparsec.empty
+
+lexeme :: Parser a -> Parser a
+lexeme = L.lexeme scn
+
+-- | Represents `dart pub deps -s compact`.
+dartPubDepCmd :: Command
+dartPubDepCmd =
+ Command
+ { cmdName = "dart"
+ , cmdArgs = ["pub", "deps", "-s", "compact"]
+ , cmdAllowErr = Never
+ }
+
+-- | Represents `flutter pub deps -s compact`.
+-- This is when, dart project has flutter app or other Flutter-specific code.
+flutterPubDepCmd :: Command
+flutterPubDepCmd =
+ Command
+ { cmdName = "flutter"
+ , cmdArgs = ["pub", "deps", "-s", "compact"]
+ , cmdAllowErr = Never
+ }
+
+-- | Represents `pub deps -s compact`.
+-- Standalone pub command is required for dart lang versions below 2.10
+-- https://github.com/dart-lang/sdk/blob/master/CHANGELOG.md#pub-1
+pubDepJsonCmd :: Command
+pubDepJsonCmd =
+ Command
+ { cmdName = "pub"
+ , cmdArgs = ["deps", "-s", "compact"]
+ , cmdAllowErr = Never
+ }
+
+data PubDepPackage = PubDepPackage
+ { pubDepPackageName :: PackageName
+ , pubDepPackageVersion :: Maybe Text
+ , pubDepPackageDeps :: Maybe (Set PackageName)
+ , pubDepPackageIsDirect :: Bool
+ }
+ deriving (Generic, Show, Eq, Ord)
+
+-- | Parse package name.
+-- Ref: https://dart.dev/tools/pub/pubspec#name
+parsePackageName :: Parser PackageName
+parsePackageName = PackageName . toText <$> many (alphaNumChar <|> char '_')
+
+-- | Parse package version.
+-- Ref: https://dart.dev/tools/pub/pubspec#version
+parsePackageVersion :: Parser Text
+parsePackageVersion = toText <$> many (alphaNumChar <|> char '.' <|> char '-' <|> char '+')
+
+-- | Parses Pub Package Entry.
+-- - pkg_name 1.0.0 [pkg_dep_one]
+-- - pkg_name 1.0.0
+-- - pkg_name 1.0.0 [pkg_dep_one, pkg_dep_two]
+parsePubDepPackage :: Bool -> Parser PubDepPackage
+parsePubDepPackage isDirectDep = do
+ packageName <- lexeme $ symbol "-" *> lexeme parsePackageName
+ packageVersion <- lexeme parsePackageVersion
+
+ -- package may not have any dependencies
+ packageSubDeps <- optional (between (symbol "[") (symbol "]") (Set.fromList <$> sepBy parsePackageName (symbol " ")))
+ pure $ PubDepPackage packageName (Just packageVersion) packageSubDeps isDirectDep
+
+-- | Parses command output.
+-- Reference: https://github.com/dart-lang/pub/blob/291705ca0b9632cb945dd39493dd5b9db41b897a/lib/src/command/deps.dart#L176
+-- Note, it prints dependencies, dev dependencies, overrides, and transitive dependencies in order.
+depsCmdOutputParser :: Parser [PubDepPackage]
+depsCmdOutputParser = parseDeps <* eof
+ where
+ parseDeps = do
+ directDeps <- skipManyTill anySingle (symbol "dependencies:") *> many (parsePubDepPackage True)
+ devDeps <- optional $ symbol "dev dependencies:" *> many (parsePubDepPackage True)
+
+ -- Since, this strategy requires lockfile - dependency override information is redundant,
+ -- as lock file already produces resolved dependency source.
+ _ <- optional $ symbol "dependency overrides:" *> many (parsePubDepPackage False)
+ transitiveDeps <- optional $ symbol "transitive dependencies:" *> many (parsePubDepPackage False)
+
+ pure $
+ directDeps
+ ++ fromMaybe [] devDeps
+ ++ fromMaybe [] transitiveDeps
+
+isPackageSupported :: PubLockContent -> PackageName -> Bool
+isPackageSupported lockContent pkg = maybe False isSupported $ Map.lookup pkg (packages lockContent)
+
+buildGraph :: PubLockContent -> [PubDepPackage] -> Graphing Dependency
+buildGraph lockContent pkgs = gmap pkgToDependency filteredGraphOfPackageNames
+ where
+ graphOfPackageNames :: Graphing PackageName
+ graphOfPackageNames =
+ directs (pubDepPackageName <$> filter pubDepPackageIsDirect pkgs)
+ -- packages without any edges
+ <> deeps (pubDepPackageName <$> filter (\x -> Set.empty == fromMaybe Set.empty (pubDepPackageDeps x)) pkgs)
+ <> edges (concatMap edgesOf pkgs)
+
+ edgesOf :: PubDepPackage -> [(PackageName, PackageName)]
+ edgesOf pkg = (pubDepPackageName pkg,) <$> toList (fromMaybe Set.empty $ pubDepPackageDeps pkg)
+
+ filteredGraphOfPackageNames :: Graphing PackageName
+ filteredGraphOfPackageNames = shrink (isReportable) graphOfPackageNames
+
+ isReportable :: PackageName -> Bool
+ isReportable = isPackageSupported lockContent
+
+ pkgToDependency :: PackageName -> Dependency
+ pkgToDependency pkg = toDependency pkg $ metadataOf pkg
+
+ metadataOf :: PackageName -> PubLockPackageMetadata
+ metadataOf pkg =
+ Map.findWithDefault
+ ( PubLockPackageMetadata
+ { pubLockPackageIsDirect = any (\x -> pubDepPackageName x == pkg) pkgs
+ , pubLockPackageSource = HostedSource $ PubLockPackageHostedSource Nothing Nothing
+ , pubLockPackageVersion = pubDepPackageVersion =<< find (\x -> pubDepPackageName x == pkg) pkgs
+ , pubLockPackageEnvironment = []
+ }
+ )
+ pkg
+ (packages lockContent)
+
+-- | Analyze using pub deps command and lockfile.
+-- The pub package manager has a command-line interface that works with either the flutter tool or the dart tool.
+-- We attempt with flutter command, than dart command, and lastly with pub (for versions below 2.10).
+-- Ref: https://dart.dev/tools/pub/cmd.
+analyzeDepsCmd ::
+ (Has Exec sig m, Has ReadFS sig m, Has Diagnostics sig m, Has Logger sig m) =>
+ Path Abs File ->
+ Path Abs Dir ->
+ m (Graphing Dependency, GraphBreadth)
+analyzeDepsCmd lockFile dir = do
+ lockContents <- context "Reading pubspec.lock" $ readContentsYaml lockFile
+ depsCmdPackages <-
+ execParser depsCmdOutputParser dir flutterPubDepCmd
+ <||> execParser depsCmdOutputParser dir dartPubDepCmd
+ <||> execParser depsCmdOutputParser dir pubDepJsonCmd
+
+ _ <- logIgnoredPackages lockContents
+
+ context "building graphing from pub deps command and pubspec.lock" $
+ pure (buildGraph lockContents depsCmdPackages, Complete)
diff --git a/src/Strategy/Dart/PubSpec.hs b/src/Strategy/Dart/PubSpec.hs
new file mode 100644
index 000000000..cb24d5683
--- /dev/null
+++ b/src/Strategy/Dart/PubSpec.hs
@@ -0,0 +1,194 @@
+module Strategy.Dart.PubSpec (
+ analyzePubSpecFile,
+
+ -- * for testing
+ buildGraph,
+ PubSpecContent (..),
+ PubSpecDepSource (..),
+ PubSpecDepHostedSource (..),
+ PubSpecDepGitSource (..),
+ PubSpecDepSdkSource (..),
+ PubSpecDepPathSource (..),
+) where
+
+import Control.Applicative ((<|>))
+import Control.Effect.Diagnostics (Diagnostics, context)
+import Data.Foldable (asum, for_)
+import Data.Map (Map, toList)
+import Data.Map.Strict qualified as Map
+import Data.Maybe (fromMaybe, maybeToList)
+import Data.Text (Text)
+import Data.Yaml (FromJSON (parseJSON), (.:), (.:?))
+import Data.Yaml qualified as Yaml
+import DepTypes (
+ DepEnvironment (EnvDevelopment, EnvProduction),
+ DepType (GitType, PubType),
+ Dependency (..),
+ VerConstraint (CEq),
+ )
+import Effect.Logger (Logger (..), Pretty (pretty), logDebug)
+import Effect.ReadFS (Has, ReadFS, readContentsYaml)
+import Graphing (Graphing, directs, induceJust)
+import Path
+import Strategy.Dart.PubSpecLock (PackageName (..))
+import Types (GraphBreadth (..))
+
+data PubSpecContent = PubSpecContent
+ { pubSpecDependencies :: Maybe (Map PackageName PubSpecDepSource)
+ , pubSpecDevDependencies :: Maybe (Map PackageName PubSpecDepSource)
+ , pubSpecDependenciesOverrides :: Maybe (Map PackageName PubSpecDepSource)
+ }
+ deriving (Show, Eq, Ord)
+
+newtype PubSpecDepSdkSource = PubSpecDepSdkSource {sdkName :: Text} deriving (Show, Eq, Ord)
+newtype PubSpecDepPathSource = PubSpecDepPathSource {hostPath :: Text} deriving (Show, Eq, Ord)
+data PubSpecDepHostedSource = PubSpecDepHostedSource
+ { version :: Maybe Text
+ , hostedName :: Maybe Text
+ , hostedUrl :: Maybe Text
+ }
+ deriving (Show, Eq, Ord)
+data PubSpecDepGitSource = PubSpecDepGitSource
+ { gitRef :: Maybe Text
+ , gitUrl :: Text
+ }
+ deriving (Show, Eq, Ord)
+
+data PubSpecDepSource
+ = HostedSource PubSpecDepHostedSource
+ | GitSource PubSpecDepGitSource
+ | SdkSource PubSpecDepSdkSource
+ | PathSource PubSpecDepPathSource
+ deriving (Show, Eq, Ord)
+
+instance FromJSON PubSpecContent where
+ parseJSON = Yaml.withObject "pubspec.yaml content" $ \o -> do
+ dependencies <- o .:? "dependencies"
+ devDependencies <- o .:? "dev_dependencies"
+ depOverrides <- o .:? "dependency_overrides"
+ pure $ PubSpecContent dependencies devDependencies depOverrides
+
+instance FromJSON PubSpecDepSource where
+ parseJSON (Yaml.String s) = pure $ HostedSource $ PubSpecDepHostedSource (Just s) Nothing Nothing
+ parseJSON (Yaml.Object o) =
+ asum
+ [ parseHostedSource o
+ , parseGitSource o
+ , parseSdkSource o
+ , parsePathSource o
+ ]
+ where
+ parseHostedSource :: Yaml.Object -> Yaml.Parser PubSpecDepSource
+ parseHostedSource ho =
+ HostedSource
+ <$> ( PubSpecDepHostedSource
+ <$> ho .: "version"
+ <*> ho .: "hosted" |> "name"
+ <*> ho .: "hosted" |> "url"
+ )
+
+ parseGitSource :: Yaml.Object -> Yaml.Parser PubSpecDepSource
+ parseGitSource go =
+ GitSource
+ <$> ( PubSpecDepGitSource
+ <$> go .: "git" |> "ref"
+ <*> go .: "git" |> "url"
+ <|> PubSpecDepGitSource Nothing
+ <$> go .: "git"
+ )
+ parseSdkSource :: Yaml.Object -> Yaml.Parser PubSpecDepSource
+ parseSdkSource so = SdkSource . PubSpecDepSdkSource <$> so .: "sdk"
+
+ parsePathSource :: Yaml.Object -> Yaml.Parser PubSpecDepSource
+ parsePathSource po = PathSource . PubSpecDepPathSource <$> po .: "path"
+
+ (|>) :: FromJSON a => Yaml.Parser Yaml.Object -> Text -> Yaml.Parser a
+ (|>) parser key = do
+ obj <- parser
+ obj .: key
+ parseJSON _ = fail "failed parsing pub package's source!"
+
+toDependency :: DepEnvironment -> PackageName -> PubSpecDepSource -> Maybe Dependency
+toDependency environment name (HostedSource (PubSpecDepHostedSource version _ url)) =
+ Just
+ Dependency
+ { dependencyType = PubType
+ , dependencyName = unPackageName name
+ , dependencyVersion = CEq <$> version
+ , dependencyLocations = maybeToList url
+ , dependencyEnvironments = [environment]
+ , dependencyTags = Map.empty
+ }
+toDependency environment _ (GitSource (PubSpecDepGitSource gitRef gitUrl)) =
+ Just
+ Dependency
+ { dependencyType = GitType
+ , dependencyName = gitUrl
+ , dependencyVersion = CEq <$> gitRef
+ , dependencyLocations = []
+ , dependencyEnvironments = [environment]
+ , dependencyTags = Map.empty
+ }
+toDependency _ _ (SdkSource _) = Nothing
+toDependency _ _ (PathSource _) = Nothing
+
+-- | Updates all of 'A''s value, with 'B' value, when key of 'A' matches with key of 'B'.
+update :: Map PackageName PubSpecDepSource -> Map PackageName PubSpecDepSource -> Map PackageName PubSpecDepSource
+update a b = Map.union (Map.intersection b a) a
+
+buildGraph :: PubSpecContent -> Graphing.Graphing Dependency
+buildGraph specContent = induceJust allDependencies
+ where
+ -- In pub manifest, dependency can be overriden.
+ -- Ref: https://dart.dev/tools/pub/dependencies#dependency-overrides
+ supersededDependencies :: Map PackageName PubSpecDepSource
+ supersededDependencies = fromMaybe Map.empty $ pubSpecDependenciesOverrides specContent
+
+ dependencies :: Map PackageName PubSpecDepSource
+ dependencies = update (fromMaybe Map.empty $ pubSpecDependencies specContent) supersededDependencies
+
+ devDependencies :: Map PackageName PubSpecDepSource
+ devDependencies = update (fromMaybe Map.empty $ pubSpecDevDependencies specContent) supersededDependencies
+
+ allDependencies :: Graphing (Maybe Dependency)
+ allDependencies =
+ directs (map (uncurry $ toDependency EnvProduction) $ toList dependencies)
+ <> directs (map (uncurry $ toDependency EnvDevelopment) $ toList devDependencies)
+
+logIgnoredPackages :: Has Logger sig m => PubSpecContent -> m ()
+logIgnoredPackages specContent = for_ notSupportedPackagesMsgs (logDebug . pretty)
+ where
+ notSupportedPackagesMsgs :: [Text]
+ notSupportedPackagesMsgs = map (<> " : ignored in analyses. Dependency's source is not supported!") notSupportedPackages
+
+ notSupportedPackages :: [Text]
+ notSupportedPackages =
+ notSupportedOf (pubSpecDevDependencies specContent)
+ ++ notSupportedOf (pubSpecDependencies specContent)
+
+ notSupportedOf :: Maybe (Map PackageName PubSpecDepSource) -> [Text]
+ notSupportedOf dependencies =
+ unPackageName . fst
+ <$> (toList . notSupported)
+ (update (fromMaybe Map.empty dependencies) supersededDependencies)
+
+ supersededDependencies :: Map PackageName PubSpecDepSource
+ supersededDependencies = fromMaybe Map.empty $ pubSpecDependenciesOverrides specContent
+
+ notSupported :: Map k PubSpecDepSource -> Map k PubSpecDepSource
+ notSupported = Map.filter $ not . isSupported
+
+ isSupported :: PubSpecDepSource -> Bool
+ isSupported (GitSource _) = True
+ isSupported (HostedSource _) = True
+ isSupported (SdkSource _) = False
+ isSupported (PathSource _) = False
+
+analyzePubSpecFile ::
+ (Has ReadFS sig m, Has Diagnostics sig m, Has Logger sig m) =>
+ Path Abs File ->
+ m (Graphing Dependency, GraphBreadth)
+analyzePubSpecFile specFile = do
+ specContent <- context "Reading pubspec.yaml" $ readContentsYaml specFile
+ _ <- logIgnoredPackages specContent
+ context "building graphing from pubspec.yaml" $ pure (buildGraph specContent, Partial)
diff --git a/src/Strategy/Dart/PubSpecLock.hs b/src/Strategy/Dart/PubSpecLock.hs
new file mode 100644
index 000000000..55e4a48d1
--- /dev/null
+++ b/src/Strategy/Dart/PubSpecLock.hs
@@ -0,0 +1,188 @@
+module Strategy.Dart.PubSpecLock (
+ analyzePubLockFile,
+ PackageName (..),
+ PubLockContent (..),
+ PubLockPackageMetadata (..),
+ logIgnoredPackages,
+
+ -- * for testing
+ buildGraph,
+ PubDepSource (..),
+ PubLockPackageHostedSource (..),
+ PubLockPackageGitSource (..),
+ PubLockPackageSdkSource (..),
+ PubLockPackagePathSource (..),
+ toDependency,
+ isSupported,
+) where
+
+import Control.Effect.Diagnostics (Diagnostics, context)
+import Data.Aeson.Types (
+ FromJSONKey,
+ )
+import Data.Aeson.Types qualified as AesonTypes
+import Data.Foldable (asum, for_)
+import Data.Map (Map)
+import Data.Map.Strict qualified as Map
+import Data.Text (Text)
+import Data.Text qualified as Text
+import Data.Yaml (FromJSON (parseJSON), (.:), (.:?))
+import Data.Yaml qualified as Yaml
+import DepTypes (
+ DepEnvironment (EnvDevelopment, EnvProduction),
+ DepType (GitType, PubType),
+ Dependency (..),
+ VerConstraint (CEq),
+ )
+import Effect.Exec (Exec, Has)
+import Effect.Logger (Logger (..), Pretty (pretty), logDebug)
+import Effect.ReadFS (ReadFS, readContentsYaml)
+import GHC.Generics (Generic)
+import Graphing (Graphing, deeps, directs)
+import Path
+import Types (GraphBreadth (..))
+
+newtype PackageName = PackageName {unPackageName :: Text} deriving (Show, Eq, Ord, FromJSONKey)
+newtype PubLockContent = PubLockContent {packages :: Map PackageName PubLockPackageMetadata} deriving (Show, Eq, Ord)
+
+-- | Represents Pub Dependency's metadata from lock file.
+data PubLockPackageMetadata = PubLockPackageMetadata
+ { pubLockPackageIsDirect :: Bool
+ , pubLockPackageSource :: PubDepSource
+ , pubLockPackageVersion :: Maybe Text
+ , pubLockPackageEnvironment :: [DepEnvironment]
+ }
+ deriving (Generic, Show, Eq, Ord)
+
+newtype PubLockPackageSdkSource = PubLockPackageSdkSource {sdkName :: Text} deriving (Show, Eq, Ord)
+newtype PubLockPackagePathSource = PubLockPackagePathSource {hostPath :: Text} deriving (Show, Eq, Ord)
+data PubLockPackageGitSource = PubLockPackageGitSource {gitUrl :: Text, ref :: Text} deriving (Show, Eq, Ord)
+data PubLockPackageHostedSource = PubLockPackageHostedSource {hostPackageName :: Maybe Text, hostUrl :: Maybe Text} deriving (Show, Eq, Ord)
+
+-- | Represents Pub Dependency's source.
+data PubDepSource
+ = SdkSource PubLockPackageSdkSource
+ | GitSource PubLockPackageGitSource
+ | HostedSource PubLockPackageHostedSource
+ | PathSource PubLockPackagePathSource
+ deriving (Show, Eq, Ord)
+
+instance FromJSON PackageName where
+ parseJSON (AesonTypes.String packageName) = pure $ PackageName packageName
+ parseJSON _ = fail "failed to parse package's name"
+
+instance FromJSON PubLockContent where
+ parseJSON = Yaml.withObject "pubspec.lock content" $ \o -> do
+ packages <- o .: "packages"
+ pure $ PubLockContent packages
+
+instance FromJSON PubLockPackageMetadata where
+ parseJSON = Yaml.withObject "pubspec.lock content package" $ \o -> do
+ pubLockPackageIsDirect <- isDirect <$> o .: "dependency"
+ pubLockPackageSource <- o .: "description"
+ pubLockPackageVersion <- o .:? "version"
+ pubLockPackageEnvironment <- getEnvironment <$> o .: "dependency"
+ pure $ PubLockPackageMetadata pubLockPackageIsDirect pubLockPackageSource pubLockPackageVersion pubLockPackageEnvironment
+ where
+ isDirect :: Text -> Bool
+ isDirect candidate = Text.isInfixOf "direct" candidate
+
+ getEnvironment :: Text -> [DepEnvironment]
+ getEnvironment candidate
+ | Text.isInfixOf "main" candidate = [EnvProduction]
+ | Text.isInfixOf "dev" candidate = [EnvDevelopment]
+ | otherwise = []
+
+instance FromJSON PubDepSource where
+ parseJSON (Yaml.String v) = pure $ SdkSource $ PubLockPackageSdkSource v
+ parseJSON (Yaml.Object o) =
+ asum
+ [ parseGitSource o
+ , parsePathSource o
+ , parseHostedSource o
+ ]
+ where
+ parseHostedSource :: Yaml.Object -> Yaml.Parser PubDepSource
+ parseHostedSource ho = HostedSource <$> (PubLockPackageHostedSource <$> ho .:? "name" <*> ho .:? "url")
+
+ parseGitSource :: Yaml.Object -> Yaml.Parser PubDepSource
+ parseGitSource go = GitSource <$> (PubLockPackageGitSource <$> go .: "url" <*> go .: "ref")
+
+ parsePathSource :: Yaml.Object -> Yaml.Parser PubDepSource
+ parsePathSource po = PathSource . PubLockPackagePathSource <$> po .: "path"
+ parseJSON _ = fail "could not parse pub dependency's metadata"
+
+isSupported :: PubLockPackageMetadata -> Bool
+isSupported meta =
+ case pubLockPackageSource meta of
+ SdkSource _ -> False
+ PathSource _ -> False
+ GitSource _ -> True
+ HostedSource _ -> True
+
+-- Transforms Package into Dependency.
+toDependency :: PackageName -> PubLockPackageMetadata -> Dependency
+toDependency pkg meta =
+ Dependency
+ { dependencyType = depType
+ , dependencyName = depName
+ , dependencyVersion = depVersion
+ , dependencyLocations = depLocation
+ , dependencyEnvironments = pubLockPackageEnvironment meta
+ , dependencyTags = Map.empty
+ }
+ where
+ depType :: DepType
+ depType = case pubLockPackageSource meta of
+ GitSource{} -> GitType
+ _ -> PubType
+
+ depName :: Text
+ depName = case pubLockPackageSource meta of
+ GitSource (PubLockPackageGitSource gitUrl _) -> gitUrl
+ _ -> unPackageName pkg
+
+ depVersion :: Maybe VerConstraint
+ depVersion = case pubLockPackageSource meta of
+ GitSource (PubLockPackageGitSource _ ref) -> Just $ CEq ref
+ _ -> CEq <$> pubLockPackageVersion meta
+
+ depLocation :: [Text]
+ depLocation = case pubLockPackageSource meta of
+ HostedSource (PubLockPackageHostedSource _ (Just hostUrl)) -> [hostUrl]
+ _ -> []
+
+-- | Builds the dependency graphing from pubspec.lock's content.
+-- Edges are not reported.
+buildGraph :: PubLockContent -> Graphing Dependency
+buildGraph lockContent = graphOfDirects <> graphOfTransitives
+ where
+ supportedPackages :: Map PackageName PubLockPackageMetadata
+ supportedPackages = Map.filter isSupported $ packages lockContent
+
+ getDependencies :: (PubLockPackageMetadata -> Bool) -> [Dependency]
+ getDependencies f = Map.elems $ Map.mapWithKey toDependency $ Map.filter f supportedPackages
+
+ graphOfTransitives :: Graphing Dependency
+ graphOfTransitives = deeps $ getDependencies $ not . pubLockPackageIsDirect
+
+ graphOfDirects :: Graphing Dependency
+ graphOfDirects = directs $ getDependencies pubLockPackageIsDirect
+
+logIgnoredPackages :: Has Logger sig m => PubLockContent -> m ()
+logIgnoredPackages lockContent = for_ notSupportedDependenciesMsgs (logDebug . pretty)
+ where
+ notSupportedDependenciesMsgs :: [Text]
+ notSupportedDependenciesMsgs = map (<> " : ignored in analyses. Dependency's source is not supported!") notSupportedPackages
+
+ notSupportedPackages :: [Text]
+ notSupportedPackages = map (unPackageName . fst) (Map.toList $ Map.filter isSupported $ packages lockContent)
+
+analyzePubLockFile ::
+ (Has Exec sig m, Has ReadFS sig m, Has Diagnostics sig m, Has Logger sig m) =>
+ Path Abs File ->
+ m (Graphing Dependency, GraphBreadth)
+analyzePubLockFile lockFile = do
+ lockContents <- context "Reading pubspec.lock" $ readContentsYaml lockFile
+ _ <- logIgnoredPackages lockContents
+ context "building graphing from pubspec.lock only" $ pure (buildGraph lockContents, Partial)
diff --git a/src/Strategy/Pub.hs b/src/Strategy/Pub.hs
new file mode 100644
index 000000000..95ab39f9e
--- /dev/null
+++ b/src/Strategy/Pub.hs
@@ -0,0 +1,60 @@
+module Strategy.Pub (discover) where
+
+import Control.Effect.Diagnostics (Diagnostics, context, (<||>))
+import Discovery.Walk (WalkStep (WalkContinue), findFileNamed, walk')
+import Effect.Exec (Exec, Has)
+import Effect.Logger (Logger (..))
+import Effect.ReadFS (ReadFS)
+import Path
+import Strategy.Dart.PubDeps (analyzeDepsCmd)
+import Strategy.Dart.PubSpec (analyzePubSpecFile)
+import Strategy.Dart.PubSpecLock (analyzePubLockFile)
+import Types (DependencyResults (..), DiscoveredProject (..))
+
+discover :: (Has ReadFS sig m, Has Diagnostics sig m, Has ReadFS rsig run, Has Exec rsig run, Has Diagnostics rsig run, Has Logger rsig run) => Path Abs Dir -> m [DiscoveredProject run]
+discover dir = context "Pub" $ do
+ projects <- context "Finding projects" $ findProjects dir
+ pure (map mkProject projects)
+
+findProjects :: (Has ReadFS sig m, Has Diagnostics sig m) => Path Abs Dir -> m [PubProject]
+findProjects = walk' $ \dir _ files -> do
+ -- Note: pub does not support pubspec.yml naming - it must be pubspec.yaml.
+ let pubSpecFile = findFileNamed "pubspec.yaml" files
+ let pubSpecLockFile = findFileNamed "pubspec.lock" files
+
+ case (pubSpecFile, pubSpecLockFile) of
+ (Just specFile, Just lockFile) -> pure ([PubProject specFile (Just lockFile) dir], WalkContinue)
+ (Just specFile, Nothing) -> pure ([PubProject specFile Nothing dir], WalkContinue)
+ -- lockfile without manifest (pubspec.yaml) is not a dart project
+ -- ref: https://dart.dev/guides/packages
+ (Nothing, Just _) -> pure ([], WalkContinue)
+ (Nothing, Nothing) -> pure ([], WalkContinue)
+
+data PubProject = PubProject
+ { pubSpec :: Path Abs File
+ , pubLock :: Maybe (Path Abs File)
+ , pubSpecDir :: Path Abs Dir
+ }
+ deriving (Eq, Ord, Show)
+
+mkProject :: (Has Exec sig n, Has ReadFS sig n, Has Diagnostics sig n, Has Logger sig n) => PubProject -> DiscoveredProject n
+mkProject project =
+ DiscoveredProject
+ { projectType = "pub"
+ , projectBuildTargets = mempty
+ , projectDependencyResults = const $ getDeps project
+ , projectPath = pubSpecDir project
+ , projectLicenses = pure []
+ }
+
+getDeps :: (Has Exec sig m, Has ReadFS sig m, Has Diagnostics sig m, Has Logger sig m) => PubProject -> m DependencyResults
+getDeps project = do
+ (graph, graphBreadth) <- case pubLock project of
+ Just lockFile -> analyzeDepsCmd lockFile (pubSpecDir project) <||> analyzePubLockFile lockFile
+ Nothing -> analyzePubSpecFile $ pubSpec project
+ pure $
+ DependencyResults
+ { dependencyGraph = graph
+ , dependencyGraphBreadth = graphBreadth
+ , dependencyManifestFiles = [pubSpec project]
+ }
diff --git a/test/Dart/PubDepsSpec.hs b/test/Dart/PubDepsSpec.hs
new file mode 100644
index 000000000..351cd057f
--- /dev/null
+++ b/test/Dart/PubDepsSpec.hs
@@ -0,0 +1,354 @@
+module Dart.PubDepsSpec (
+ spec,
+) where
+
+import Data.Map.Strict qualified as Map
+import Data.Set qualified as Set
+import Data.Text.IO qualified as TIO
+import DepTypes
+import GraphUtil (expectDeps, expectDirect, expectEdges)
+import Strategy.Dart.PubDeps (PubDepPackage (..), buildGraph, depsCmdOutputParser)
+import Strategy.Dart.PubSpecLock (
+ PackageName (..),
+ PubDepSource (..),
+ PubLockContent (..),
+ PubLockPackageHostedSource (..),
+ PubLockPackageMetadata (..),
+ PubLockPackagePathSource (..),
+ PubLockPackageSdkSource (..),
+ )
+import Test.Hspec
+import Text.Megaparsec
+
+expectedDepsCmdOutput :: [PubDepPackage]
+expectedDepsCmdOutput =
+ [ PubDepPackage (PackageName "pkg_a") (Just "5.0.0") (Just $ Set.fromList [PackageName "pkg_aa"]) True
+ , PubDepPackage (PackageName "pkg_b") (Just "4.0.0") Nothing True
+ , PubDepPackage (PackageName "pkg_c") (Just "3.0.0") (Just $ Set.fromList [PackageName "pkg_ca", PackageName "pkg_cb"]) True
+ , PubDepPackage (PackageName "pkg_d") (Just "3.0.1") Nothing True
+ , PubDepPackage (PackageName "pkg_e") (Just "2.0.0") (Just $ Set.fromList [PackageName "pkg_ea"]) True
+ , PubDepPackage (PackageName "pkg_aa") (Just "1.0.0") (Just $ Set.fromList [PackageName "pkg_ca"]) False
+ , PubDepPackage (PackageName "pkg_ca") (Just "1.2.0") Nothing False
+ , PubDepPackage (PackageName "pkg_cb") (Just "1.3.0") Nothing False
+ , PubDepPackage (PackageName "pkg_ea") (Just "1.4.0") Nothing False
+ ]
+
+spec :: Spec
+spec = do
+ describe "pub deps -s compact" $ do
+ contents <- runIO (TIO.readFile "test/Dart/testdata/pubdeps.compact")
+
+ it "should parse content correctly" $ do
+ case runParser depsCmdOutputParser "" contents of
+ Left failCode -> expectationFailure $ show failCode
+ Right result -> result `shouldBe` expectedDepsCmdOutput
+
+ describe "buildGraph" $ do
+ it "should build graph with edges" $ do
+ let pubDepsContent =
+ [ PubDepPackage (PackageName "pkg_a") (Just "1.8.0") (Just $ Set.fromList [PackageName "pkg_deep"]) True
+ , PubDepPackage (PackageName "pkg_deep") (Just "1.10.0") (Just $ Set.fromList [PackageName "pkg_deeper"]) False
+ , PubDepPackage (PackageName "pkg_deeper") (Just "1.20.0") Nothing False
+ ]
+
+ let lockContent =
+ PubLockContent
+ { packages =
+ Map.fromList
+ [
+ ( PackageName "pkg_a"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = True
+ , pubLockPackageSource = HostedSource $ PubLockPackageHostedSource (Just "pkg_a") (Just "https://pub.dartlang.org")
+ , pubLockPackageVersion = Just "1.8.0"
+ , pubLockPackageEnvironment = []
+ }
+ )
+ ,
+ ( PackageName "pkg_deep"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = False
+ , pubLockPackageSource = HostedSource $ PubLockPackageHostedSource (Just "pkg_deep") (Just "https://pub.dartlang.org")
+ , pubLockPackageVersion = Just "1.10.0"
+ , pubLockPackageEnvironment = []
+ }
+ )
+ ,
+ ( PackageName "pkg_deeper"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = False
+ , pubLockPackageSource = HostedSource $ PubLockPackageHostedSource (Just "pkg_deep") (Just "https://pub.dartlang.org")
+ , pubLockPackageVersion = Just "1.20.0"
+ , pubLockPackageEnvironment = []
+ }
+ )
+ ]
+ }
+ let graph = buildGraph lockContent pubDepsContent
+
+ expectDirect
+ [ Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_a"
+ , dependencyVersion = Just $ CEq "1.8.0"
+ , dependencyLocations = ["https://pub.dartlang.org"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ ]
+ graph
+
+ expectDeps
+ [ Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_a"
+ , dependencyVersion = Just $ CEq "1.8.0"
+ , dependencyLocations = ["https://pub.dartlang.org"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ , Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_deep"
+ , dependencyVersion = Just $ CEq "1.10.0"
+ , dependencyLocations = ["https://pub.dartlang.org"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ , Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_deeper"
+ , dependencyVersion = Just $ CEq "1.20.0"
+ , dependencyLocations = ["https://pub.dartlang.org"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ ]
+ graph
+
+ expectEdges
+ [
+ ( Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_a"
+ , dependencyVersion = Just $ CEq "1.8.0"
+ , dependencyLocations = ["https://pub.dartlang.org"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ , Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_deep"
+ , dependencyVersion = Just $ CEq "1.10.0"
+ , dependencyLocations = ["https://pub.dartlang.org"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ )
+ ,
+ ( Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_deep"
+ , dependencyVersion = Just $ CEq "1.10.0"
+ , dependencyLocations = ["https://pub.dartlang.org"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ , Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_deeper"
+ , dependencyVersion = Just $ CEq "1.20.0"
+ , dependencyLocations = ["https://pub.dartlang.org"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ )
+ ]
+ graph
+
+ it "should ignore path dependency" $ do
+ let pubDepsContent = [PubDepPackage (PackageName "pkg_path") (Just "1.10.0") Nothing True]
+ let lockContent =
+ PubLockContent
+ { packages =
+ Map.fromList
+ [
+ ( PackageName "pkg_path"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = True
+ , pubLockPackageSource = PathSource $ PubLockPackagePathSource "./../dir"
+ , pubLockPackageVersion = Just "1.8.0"
+ , pubLockPackageEnvironment = []
+ }
+ )
+ ]
+ }
+
+ let graph = buildGraph lockContent pubDepsContent
+ expectDirect [] graph
+ expectDeps [] graph
+ expectEdges [] graph
+
+ it "should ignore sdk dependency" $ do
+ let pubDepsContent = [PubDepPackage (PackageName "pkg_sdk") (Just "1.10.0") Nothing True]
+
+ let lockContent =
+ PubLockContent
+ { packages =
+ Map.fromList
+ [
+ ( PackageName "pkg_sdk"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = True
+ , pubLockPackageSource = SdkSource $ PubLockPackageSdkSource "pkg_sdk_source"
+ , pubLockPackageVersion = Just "1.8.0"
+ , pubLockPackageEnvironment = []
+ }
+ )
+ ]
+ }
+
+ let graph = buildGraph lockContent pubDepsContent
+ expectDirect [] graph
+ expectDeps [] graph
+ expectEdges [] graph
+
+ it "should build graph when dependencies have no edges" $ do
+ let pubDepsContent = [PubDepPackage (PackageName "pkg_a") (Just "1.10.0") Nothing True]
+
+ let lockContent =
+ PubLockContent
+ { packages =
+ Map.fromList
+ [
+ ( PackageName "pkg_a"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = True
+ , pubLockPackageSource = HostedSource $ PubLockPackageHostedSource Nothing Nothing
+ , pubLockPackageVersion = Just "1.8.0"
+ , pubLockPackageEnvironment = []
+ }
+ )
+ ]
+ }
+
+ let graph = buildGraph lockContent pubDepsContent
+ expectDirect
+ [ Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_a"
+ , dependencyVersion = Just $ CEq "1.8.0"
+ , dependencyLocations = []
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ ]
+ graph
+ expectDeps
+ [ Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_a"
+ , dependencyVersion = Just $ CEq "1.8.0"
+ , dependencyLocations = []
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ ]
+ graph
+ expectEdges [] graph
+
+ it "should retain supported transitive dependencies, even if predecessor is not supported" $ do
+ let pubDepsContent =
+ [ PubDepPackage (PackageName "pkg_a") (Just "1.8.0") (Just $ Set.fromList [PackageName "pkg_deep"]) True
+ , PubDepPackage (PackageName "pkg_deep") (Just "1.10.0") (Just $ Set.fromList [PackageName "pkg_deeper"]) False
+ , PubDepPackage (PackageName "pkg_deeper") (Just "1.20.0") Nothing False
+ ]
+
+ let lockContent =
+ PubLockContent
+ { packages =
+ Map.fromList
+ [
+ ( PackageName "pkg_a"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = True
+ , pubLockPackageSource = HostedSource $ PubLockPackageHostedSource (Just "pkg_a") (Just "https://pub.dartlang.org")
+ , pubLockPackageVersion = Just "1.8.0"
+ , pubLockPackageEnvironment = []
+ }
+ )
+ ,
+ ( PackageName "pkg_deep"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = False
+ , pubLockPackageSource = PathSource $ PubLockPackagePathSource "some/path/"
+ , pubLockPackageVersion = Just "1.10.0"
+ , pubLockPackageEnvironment = []
+ }
+ )
+ ,
+ ( PackageName "pkg_deeper"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = False
+ , pubLockPackageSource = HostedSource $ PubLockPackageHostedSource (Just "pkg_deep") (Just "https://pub.dartlang.org")
+ , pubLockPackageVersion = Just "1.20.0"
+ , pubLockPackageEnvironment = []
+ }
+ )
+ ]
+ }
+ let graph = buildGraph lockContent pubDepsContent
+
+ expectDirect
+ [ Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_a"
+ , dependencyVersion = Just $ CEq "1.8.0"
+ , dependencyLocations = ["https://pub.dartlang.org"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ ]
+ graph
+
+ expectDeps
+ [ Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_a"
+ , dependencyVersion = Just $ CEq "1.8.0"
+ , dependencyLocations = ["https://pub.dartlang.org"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ , Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_deeper"
+ , dependencyVersion = Just $ CEq "1.20.0"
+ , dependencyLocations = ["https://pub.dartlang.org"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ ]
+ graph
+
+ expectEdges
+ [
+ ( Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_a"
+ , dependencyVersion = Just $ CEq "1.8.0"
+ , dependencyLocations = ["https://pub.dartlang.org"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ , Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_deeper"
+ , dependencyVersion = Just $ CEq "1.20.0"
+ , dependencyLocations = ["https://pub.dartlang.org"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ )
+ ]
+ graph
diff --git a/test/Dart/PubSpecLockSpec.hs b/test/Dart/PubSpecLockSpec.hs
new file mode 100644
index 000000000..43e0e3d5b
--- /dev/null
+++ b/test/Dart/PubSpecLockSpec.hs
@@ -0,0 +1,210 @@
+module Dart.PubSpecLockSpec (
+ spec,
+) where
+
+import Data.ByteString qualified as BS
+import Data.Map qualified as Map
+import Data.Yaml (decodeEither')
+import DepTypes
+import GraphUtil (expectDeps, expectDirect)
+import Graphing (empty)
+import Strategy.Dart.PubSpecLock (
+ PackageName (..),
+ PubDepSource (..),
+ PubLockContent (..),
+ PubLockPackageGitSource (..),
+ PubLockPackageHostedSource (..),
+ PubLockPackageMetadata (..),
+ PubLockPackagePathSource (..),
+ PubLockPackageSdkSource (..),
+ buildGraph,
+ toDependency,
+ )
+import Test.Hspec
+
+expectedLockFile :: PubLockContent
+expectedLockFile =
+ PubLockContent
+ { packages =
+ Map.fromList
+ [
+ ( PackageName "pkg_hosted"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = False
+ , pubLockPackageSource = HostedSource $ PubLockPackageHostedSource (Just "pkg_hosted") (Just "https://pub.dartlang.org")
+ , pubLockPackageVersion = Just "1.1"
+ , pubLockPackageEnvironment = []
+ }
+ )
+ ,
+ ( PackageName "pkg_git"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = False
+ , pubLockPackageSource = GitSource $ PubLockPackageGitSource "https://github.com/user/pkg" "release-0.9"
+ , pubLockPackageVersion = Just "1.2"
+ , pubLockPackageEnvironment = []
+ }
+ )
+ ,
+ ( PackageName "pkg_sdk"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = False
+ , pubLockPackageSource = SdkSource $ PubLockPackageSdkSource "flutter"
+ , pubLockPackageVersion = Just "1.3"
+ , pubLockPackageEnvironment = []
+ }
+ )
+ ,
+ ( PackageName "pkg_file"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = False
+ , pubLockPackageSource = PathSource $ PubLockPackagePathSource "/Users/dir/pkg_dir"
+ , pubLockPackageVersion = Just "1.4"
+ , pubLockPackageEnvironment = []
+ }
+ )
+ ,
+ ( PackageName "pkg_hosted_direct"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = True
+ , pubLockPackageSource = HostedSource $ PubLockPackageHostedSource (Just "pkg_hosted_direct") (Just "https://pub.dartlang.org")
+ , pubLockPackageVersion = Just "1.5"
+ , pubLockPackageEnvironment = [EnvProduction]
+ }
+ )
+ ,
+ ( PackageName "pkg_hosted_direct_dev"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = True
+ , pubLockPackageSource = HostedSource $ PubLockPackageHostedSource (Just "pkg_hosted_direct_dev") (Just "https://pub.dartlang.org")
+ , pubLockPackageVersion = Just "1.6"
+ , pubLockPackageEnvironment = [EnvDevelopment]
+ }
+ )
+ ]
+ }
+
+spec :: Spec
+spec = do
+ lockFile <- runIO (BS.readFile "test/Dart/testdata/pubspec.lock")
+
+ describe "pubspec.lock parsing" $
+ it "should parse file correctly" $
+ case decodeEither' lockFile of
+ Right res -> res `shouldBe` expectedLockFile
+ Left err -> expectationFailure $ "failed to parse: " <> show err
+
+ describe "pubLockPackage to dependency conversion" $ do
+ it "should create dependency for hosted sources" $ do
+ let pkg =
+ PubLockPackageMetadata
+ { pubLockPackageIsDirect = True
+ , pubLockPackageSource = HostedSource $ PubLockPackageHostedSource (Just "pkg_a") (Just "https://pub.dartlang.org")
+ , pubLockPackageVersion = Just "1.1"
+ , pubLockPackageEnvironment = [EnvDevelopment]
+ }
+ let expectedDependency =
+ Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_a"
+ , dependencyVersion = Just $ CEq "1.1"
+ , dependencyLocations = ["https://pub.dartlang.org"]
+ , dependencyEnvironments = [EnvDevelopment]
+ , dependencyTags = Map.empty
+ }
+ toDependency (PackageName "pkg_a") pkg `shouldBe` expectedDependency
+
+ it "should create dependency for git sources" $ do
+ let pkg =
+ PubLockPackageMetadata
+ { pubLockPackageIsDirect = True
+ , pubLockPackageSource = GitSource $ PubLockPackageGitSource "https://github.com/user/pkg" "release-0.9"
+ , pubLockPackageVersion = Just "1.1"
+ , pubLockPackageEnvironment = []
+ }
+ let expectedDependency =
+ Dependency
+ { dependencyType = GitType
+ , dependencyName = "https://github.com/user/pkg"
+ , dependencyVersion = Just $ CEq "release-0.9"
+ , dependencyLocations = []
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ toDependency (PackageName "pkg_b") pkg `shouldBe` expectedDependency
+
+ describe "graphing from pubspec.lock" $ do
+ it "should not create graphing for packages sourced from sdk" $ do
+ let sdkSources =
+ PubLockContent $
+ Map.filterWithKey (\x _ -> (unPackageName x) == "pkg_sdk") (packages expectedLockFile)
+
+ Map.null (packages sdkSources) `shouldBe` False
+ buildGraph sdkSources `shouldBe` Graphing.empty
+
+ it "should not create graphing for packages sourced from file" $ do
+ let fileSources =
+ PubLockContent $
+ Map.filterWithKey (\x _ -> (unPackageName x) == "pkg_file") (packages expectedLockFile)
+
+ Map.null (packages fileSources) `shouldBe` False
+ buildGraph fileSources `shouldBe` Graphing.empty
+
+ it "should graph direct and deep dependencies" $ do
+ let lockContent =
+ PubLockContent
+ { packages =
+ Map.fromList
+ [
+ ( PackageName "pkg_direct"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = True
+ , pubLockPackageSource = HostedSource $ PubLockPackageHostedSource (Just "pkg_direct") (Just "some-url-1")
+ , pubLockPackageVersion = Nothing
+ , pubLockPackageEnvironment = []
+ }
+ )
+ ,
+ ( PackageName "pkg_deep"
+ , PubLockPackageMetadata
+ { pubLockPackageIsDirect = False
+ , pubLockPackageSource = HostedSource $ PubLockPackageHostedSource (Just "pkg_deep") (Just "some-url-2")
+ , pubLockPackageVersion = Nothing
+ , pubLockPackageEnvironment = []
+ }
+ )
+ ]
+ }
+ let graph = buildGraph lockContent
+
+ expectDirect
+ [ Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_direct"
+ , dependencyVersion = Nothing
+ , dependencyLocations = ["some-url-1"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ ]
+ graph
+
+ expectDeps
+ [ Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_direct"
+ , dependencyVersion = Nothing
+ , dependencyLocations = ["some-url-1"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ , Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_deep"
+ , dependencyVersion = Nothing
+ , dependencyLocations = ["some-url-2"]
+ , dependencyEnvironments = []
+ , dependencyTags = Map.empty
+ }
+ ]
+ graph
diff --git a/test/Dart/PubSpecSpec.hs b/test/Dart/PubSpecSpec.hs
new file mode 100644
index 000000000..de882edf6
--- /dev/null
+++ b/test/Dart/PubSpecSpec.hs
@@ -0,0 +1,169 @@
+module Dart.PubSpecSpec (
+ spec,
+) where
+
+import Data.ByteString qualified as BS
+import Data.Map.Strict qualified as Map
+import Data.Yaml (decodeEither')
+import DepTypes
+import GraphUtil (expectDeps, expectDirect, expectEdges)
+import Strategy.Dart.PubSpec (
+ PubSpecContent (..),
+ PubSpecDepGitSource (..),
+ PubSpecDepHostedSource (..),
+ PubSpecDepPathSource (..),
+ PubSpecDepSdkSource (..),
+ PubSpecDepSource (..),
+ buildGraph,
+ )
+import Strategy.Dart.PubSpecLock (PackageName (..))
+import Test.Hspec
+
+-- hostedSource :: (Maybe VerConstraint) -> Maybe Text -> Maybe Text
+
+spec :: Spec
+spec = do
+ specFile <- runIO (BS.readFile "test/Dart/testdata/pubspec.yaml")
+ describe "parse pubspec.yml" $
+ it "should parse dependencies" $ do
+ let expectedPubSpecContent =
+ PubSpecContent
+ { pubSpecDependencies =
+ Just $
+ Map.fromList
+ [ (PackageName "pkg_default", HostedSource $ PubSpecDepHostedSource (Just "1.3.0") Nothing Nothing)
+ , (PackageName "pkg_hosted", HostedSource $ PubSpecDepHostedSource (Just "^1.0.0") (Just "pkg_hosted") (Just "http://pub.dev"))
+ , (PackageName "pkg_a", GitSource $ PubSpecDepGitSource Nothing "https://github.com/user/pkg_a.git")
+ , (PackageName "pkg_b", GitSource $ PubSpecDepGitSource (Just "release-0.9") "https://github.com/user/pkg_b")
+ , (PackageName "pkg_sdk", SdkSource $ PubSpecDepSdkSource "flutter")
+ ]
+ , pubSpecDevDependencies =
+ Just $
+ Map.fromList
+ [ (PackageName "pkg_dev_default", HostedSource $ PubSpecDepHostedSource (Just "1.0.0") Nothing Nothing)
+ ]
+ , pubSpecDependenciesOverrides =
+ Just $
+ Map.fromList
+ [ (PackageName "pkg_b", PathSource $ PubSpecDepPathSource "./some/dir")
+ ]
+ }
+
+ case decodeEither' specFile of
+ Right res -> res `shouldBe` expectedPubSpecContent
+ Left err -> expectationFailure $ "failed to parse: " <> show err
+
+ describe "build graph from pubspec.yml" $ do
+ it "should create expected graph" $ do
+ let pubSpecContent =
+ PubSpecContent
+ { pubSpecDependencies =
+ Just $
+ Map.fromList
+ [ (PackageName "pkg_default", HostedSource $ PubSpecDepHostedSource (Just "1.3.0") Nothing Nothing)
+ , (PackageName "pkg_hosted", HostedSource $ PubSpecDepHostedSource (Just "^1.0.0") (Just "pkg_hosted") (Just "http://pub.dev"))
+ , (PackageName "pkg_a", GitSource $ PubSpecDepGitSource Nothing "https://github.com/user/pkg_a.git")
+ ]
+ , pubSpecDevDependencies = Just $ Map.fromList [(PackageName "pkg_dev_default", HostedSource $ PubSpecDepHostedSource (Just "^1.0.0") Nothing Nothing)]
+ , pubSpecDependenciesOverrides = Nothing
+ }
+
+ let graph = buildGraph pubSpecContent
+ let expectedGraphDeps =
+ [ Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_default"
+ , dependencyVersion = Just $ CEq "1.3.0"
+ , dependencyLocations = []
+ , dependencyEnvironments = [EnvProduction]
+ , dependencyTags = Map.empty
+ }
+ , Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_hosted"
+ , dependencyVersion = Just $ CEq "^1.0.0"
+ , dependencyLocations = ["http://pub.dev"]
+ , dependencyEnvironments = [EnvProduction]
+ , dependencyTags = Map.empty
+ }
+ , Dependency
+ { dependencyType = GitType
+ , dependencyName = "https://github.com/user/pkg_a.git"
+ , dependencyVersion = Nothing
+ , dependencyLocations = []
+ , dependencyEnvironments = [EnvProduction]
+ , dependencyTags = Map.empty
+ }
+ , Dependency
+ { dependencyType = PubType
+ , dependencyName = "pkg_dev_default"
+ , dependencyVersion = Just $ CEq "^1.0.0"
+ , dependencyLocations = []
+ , dependencyEnvironments = [EnvDevelopment]
+ , dependencyTags = Map.empty
+ }
+ ]
+ expectEdges [] graph
+ expectDeps expectedGraphDeps graph
+ expectDirect expectedGraphDeps graph
+
+ it "should not graph, if dependency is overriden, and the new source is not supported" $ do
+ let pubSpecContent =
+ PubSpecContent
+ { pubSpecDependencies = Just $ Map.fromList [(PackageName "pkg_b", GitSource $ PubSpecDepGitSource (Just "release-0.9") "https://github.com/user/pkg_b")]
+ , pubSpecDependenciesOverrides = Just $ Map.fromList [(PackageName "pkg_b", PathSource $ PubSpecDepPathSource "./some/dir")]
+ , pubSpecDevDependencies = Nothing
+ }
+
+ let graph = buildGraph pubSpecContent
+ expectEdges [] graph
+ expectDeps [] graph
+ expectDirect [] graph
+
+ it "should graph, if dependency is overriden, and the new source is supported" $ do
+ let pubSpecContent =
+ PubSpecContent
+ { pubSpecDependencies =
+ Just $ Map.fromList [(PackageName "pkg_b", GitSource $ PubSpecDepGitSource (Just "release-0.9") "https://github.com/user/pkg_b")]
+ , pubSpecDependenciesOverrides =
+ Just $ Map.fromList [(PackageName "pkg_b", GitSource $ PubSpecDepGitSource (Just "develop") "https://github.com/user/pkg_b")]
+ , pubSpecDevDependencies = Nothing
+ }
+ let graph = buildGraph pubSpecContent
+ let expectedGraphDeps =
+ [ Dependency
+ { dependencyType = GitType
+ , dependencyName = "https://github.com/user/pkg_b"
+ , dependencyVersion = Just $ CEq "develop"
+ , dependencyLocations = []
+ , dependencyEnvironments = [EnvProduction]
+ , dependencyTags = Map.empty
+ }
+ ]
+ expectEdges [] graph
+ expectDeps expectedGraphDeps graph
+ expectDirect expectedGraphDeps graph
+
+ it "should not graph dependency of path sources" $ do
+ let pubSpecContent =
+ PubSpecContent
+ { pubSpecDependencies = Just $ Map.fromList [(PackageName "pkg_sdk", PathSource $ PubSpecDepPathSource "./../some-dir/")]
+ , pubSpecDevDependencies = Nothing
+ , pubSpecDependenciesOverrides = Nothing
+ }
+ let graph = buildGraph pubSpecContent
+ expectEdges [] graph
+ expectDeps [] graph
+ expectDirect [] graph
+
+ it "should not graph dependency of sdk sources" $ do
+ let pubSpecContent =
+ PubSpecContent
+ { pubSpecDependencies = Just $ Map.fromList [(PackageName "pkg_sdk", SdkSource $ PubSpecDepSdkSource "flutter")]
+ , pubSpecDevDependencies = Nothing
+ , pubSpecDependenciesOverrides = Nothing
+ }
+ let graph = buildGraph pubSpecContent
+ expectEdges [] graph
+ expectDeps [] graph
+ expectDirect [] graph
diff --git a/test/Dart/testdata/pubdeps.compact b/test/Dart/testdata/pubdeps.compact
new file mode 100644
index 000000000..752e39255
--- /dev/null
+++ b/test/Dart/testdata/pubdeps.compact
@@ -0,0 +1,21 @@
+Dart SDK 2.14.0-301.0.dev
+Flutter SDK 2.4.0-4.0.pre
+some_example 1.0.0+1
+
+dependencies:
+- pkg_a 5.0.0 [pkg_aa]
+- pkg_b 4.0.0
+- pkg_c 3.0.0 [pkg_ca pkg_cb]
+- pkg_d 3.0.1
+
+dev dependencies:
+- pkg_e 2.0.0 [pkg_ea]
+
+dependency overrides:
+- pkg_d 3.0.1
+
+transitive dependencies:
+- pkg_aa 1.0.0 [pkg_ca]
+- pkg_ca 1.2.0
+- pkg_cb 1.3.0
+- pkg_ea 1.4.0
diff --git a/test/Dart/testdata/pubdeps.json b/test/Dart/testdata/pubdeps.json
new file mode 100644
index 000000000..439eb8993
--- /dev/null
+++ b/test/Dart/testdata/pubdeps.json
@@ -0,0 +1,67 @@
+{
+ "root": "main_app",
+ "packages": [
+ {
+ "name": "main_app",
+ "version": "2.0.0",
+ "kind": "root",
+ "source": "root",
+ "dependencies": [
+ "pkg_a",
+ "pkg_dev_a",
+ "pkg_git",
+ "pkg_flutter"
+ ]
+ },
+ {
+ "name": "pkg_a",
+ "version": "1.8.0",
+ "kind": "direct",
+ "source": "hosted",
+ "dependencies": ["pkg_deep"]
+ },
+ {
+ "name": "pkg_dev_a",
+ "version": "3.2.0",
+ "kind": "dev",
+ "source": "hosted",
+ "dependencies": []
+ },
+ {
+ "name": "pkg_git",
+ "version": "1.8.0",
+ "kind": "direct",
+ "source": "git",
+ "dependencies": []
+ },
+ {
+ "name": "pkg_flutter",
+ "version": "1.8.0",
+ "kind": "direct",
+ "source": "sdk",
+ "dependencies": []
+ },
+ {
+ "name": "pkg_deep",
+ "version": "1.10.0",
+ "kind": "transitive",
+ "source": "hosted",
+ "dependencies": []
+ }
+ ],
+ "sdks": [
+ {
+ "name": "Dart",
+ "version": "2.14.0-301.0.dev"
+ },
+ {
+ "name": "Flutter",
+ "version": "2.4.0-4.0.pre"
+ }
+ ],
+ "executables": [
+ "test",
+ "build_runner"
+ ]
+ }
+
\ No newline at end of file
diff --git a/test/Dart/testdata/pubspec.lock b/test/Dart/testdata/pubspec.lock
new file mode 100644
index 000000000..d7f7003a0
--- /dev/null
+++ b/test/Dart/testdata/pubspec.lock
@@ -0,0 +1,61 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+
+ # hosted source
+ pkg_hosted:
+ dependency: transitive
+ description:
+ name: pkg_hosted
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1"
+
+ # git source
+ pkg_git:
+ dependency: transitive
+ description:
+ path: "."
+ ref: "release-0.9"
+ resolved-ref: "6af9eab77cf8d777b35c349b75a9c345de967ed4"
+ url: "https://github.com/user/pkg"
+ source: hosted
+ version: "1.2"
+
+ # sdk source
+ pkg_sdk:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "1.3"
+
+ # file source
+ pkg_file:
+ dependency: transitive
+ description:
+ path: "/Users/dir/pkg_dir"
+ relative: false
+ source: path
+ version: "1.4"
+
+ # direct dependency
+ pkg_hosted_direct:
+ dependency: "direct main"
+ description:
+ name: pkg_hosted_direct
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.5"
+
+ # direct development dependency
+ pkg_hosted_direct_dev:
+ dependency: "direct dev"
+ description:
+ name: pkg_hosted_direct_dev
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.6"
+
+sdks:
+ dart: ">=2.12.0 <3.0.0"
+ flutter: ">=1.16.0"
\ No newline at end of file
diff --git a/test/Dart/testdata/pubspec.yaml b/test/Dart/testdata/pubspec.yaml
new file mode 100644
index 000000000..2d36b1bdb
--- /dev/null
+++ b/test/Dart/testdata/pubspec.yaml
@@ -0,0 +1,32 @@
+name: some_example
+description: A Flutter sample to demonstrate isolates
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.0.0 <3.0.0"
+
+dependencies:
+ pkg_default: 1.3.0
+ pkg_hosted:
+ hosted:
+ name: pkg_hosted
+ url: http://pub.dev
+ version: ^1.0.0
+ pkg_a:
+ git: https://github.com/user/pkg_a.git
+ pkg_b:
+ git:
+ url: https://github.com/user/pkg_b
+ ref: release-0.9
+ pkg_sdk:
+ sdk: flutter
+
+dev_dependencies:
+ pkg_dev_default: 1.0.0
+
+dependency_overrides:
+ pkg_b:
+ path: ./some/dir
+
+flutter:
+ uses-material-design: true
\ No newline at end of file