From 4bc81ad49b816be868c0cda8da76f3d1a45d4b63 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 13 Nov 2026 13:02:13 +0000 Subject: [PATCH] fix: apply pubspec dependency_overrides in Dart scans dependency_overrides was ignored because any top-level key other than dependencies and dev_dependencies reset the parser section. Overrides now use the same entry forms, replace a declared spec while keeping its production or development flag, and add override-only packages as production rows. A present pubspec.lock still supplies the resolved version. Signed-off-by: Cursor Agent Co-authored-by: ABIZ - Agentic Business Record Standard --- docs/sbom-dependency-scope.md | 7 +- src/core-open/scanners/dart-scanner.ts | 86 ++++++++--- test/dart-pubspec-map.test.ts | 201 ++++++++++++++++++++++++- 3 files changed, 272 insertions(+), 22 deletions(-) diff --git a/docs/sbom-dependency-scope.md b/docs/sbom-dependency-scope.md index 7e87539..49dfb99 100644 --- a/docs/sbom-dependency-scope.md +++ b/docs/sbom-dependency-scope.md @@ -87,8 +87,9 @@ When the same name and version is declared in more than one section, one mark is | `mix.exs` `only:` an atom or a list that includes an environment other than `:dev` and `:test`, such as `only: [:dev, :prod]` | Stays section `dependencies`, or `optionalDependencies` when `optional: true`. Exported. | Same CycloneDX fields as that section. | Same SPDX relationship as that section. | | `mix.exs` `git:`, `github:`, `path:`, or `in_umbrella:` | Not a Hex row. A `path:` value or `in_umbrella: true` that points at another Mix project in the same scan is a project reference relative to the scan root. `git:` and `github:` stay absent. | Absent. `mix.lock` is not expanded, so a package that appears only there is absent. | Absent. | | .NET `` in a `.csproj`, `.vbproj`, or `.sqlproj` | `PrivateAssets` whose trimmed value is `all`, in any letter case, on the attribute or the child element, is section `devDependencies`. Any other value, including `compile`, and a missing `PrivateAssets`, is section `dependencies`. The same package referenced again in that project without `PrivateAssets=all` stays section `dependencies`, and that production reference supplies the version. A `packages.config` entry is section `dependencies`. A `` with no `Version` takes the version from the nearest `Directory.Packages.props` `` between the project directory and the scan root. Package ids match without regard to case. `VersionOverride` on the reference wins over that central version and over a `Version` on the same reference. A reference whose version cannot be resolved stays in the scan with version `unknown`. Exported. | `vibgrate:scope` = `direct`. Component `scope` = `excluded` on a development row. Omitted on a production row. `dependencies` omitted. The export list has no NuGet lockfile. | Annotation `scope=direct`. `DEV_DEPENDENCY_OF` from a development row to `SPDXRef-DOCUMENT`. A production row has no document relationship. | -| `pubspec.yaml` `dependencies` | Section `dependencies`. Exported. An SDK entry (`flutter: { sdk: flutter }`, a nested `sdk: flutter`, or `sdk: flutter` on the same line) is skipped. A name also listed under `dev_dependencies` is stored once, as this section, and this declaration supplies the spec. | `vibgrate:scope` = `direct`. Component `scope` omitted. `dependencies` omitted. The export list has no `pubspec.lock`. | Annotation `scope=direct`. No document relationship. | +| `pubspec.yaml` `dependencies` | Section `dependencies`. Exported. An SDK entry (`flutter: { sdk: flutter }`, a nested `sdk: flutter`, or `sdk: flutter` on the same line) is skipped. A name also listed under `dev_dependencies` is stored once, as this section. This declaration supplies the spec unless `dependency_overrides` replaces it. | `vibgrate:scope` = `direct`. Component `scope` omitted. `dependencies` omitted. The export list has no `pubspec.lock`. | Annotation `scope=direct`. No document relationship. | | `pubspec.yaml` `dev_dependencies` | Section `devDependencies`. Exported. An SDK entry (`flutter_test: { sdk: flutter }`, a nested `sdk: flutter`, or `sdk: flutter` on the same line) is skipped, the same as an SDK entry under `dependencies`. A name also listed under `dependencies` is stored once, as `dependencies`. pub.dev lookups and drift age buckets use production rows only. | `vibgrate:scope` = `direct`. Component `scope` = `excluded`. Omitted when the kept row is production. `dependencies` omitted. | Annotation `scope=direct`. `DEV_DEPENDENCY_OF` from a development row to `SPDXRef-DOCUMENT`. A production row has no document relationship. | +| `pubspec.yaml` `dependency_overrides` | Same entry forms as `dependencies` (one-line version, `{ version: … }`, nested `version:`, `path:`, and `git:`). An SDK entry is skipped and does not replace a declared spec. A package also declared under `dependencies` or `dev_dependencies` keeps that section, and the override supplies the spec. A package listed only here is section `dependencies`. A `pubspec.lock` version still supplies `resolvedVersion`. | Same CycloneDX fields as the section the row lands in. | Same SPDX relationship as that section. | | `packages.lock.json` | `Direct` and `Transitive` entries under `dependencies..`. `Project` entries are skipped. The version is `resolved`. The same package id and version under more than one target framework is one component. Package ids match without regard to case. The kept spelling is the code-unit minimum of the spellings in the file. | `direct` when a parsed manifest row has that name and version, otherwise `transitive`. `contentHash` is a SHA-512 digest on that component. `dependencies` follow each entry's `dependencies` map when the named package is resolved in this file. A `Direct` entry is a root dependency. | Annotation `scope=direct` or `scope=transitive`. `DEPENDS_ON` follows that map. Manifest `DEV_DEPENDENCY_OF` is still written from `PrivateAssets`. | `--no-transitive` keeps the manifest rows (development, optional, and peer included) and omits lockfile-only rows. It omits CycloneDX `dependencies`. SPDX still writes `DEV_DEPENDENCY_OF` and `OPTIONAL_DEPENDENCY_OF` for those manifest rows, and it omits `DEPENDS_ON` because the lockfile graph is not read. @@ -148,7 +149,7 @@ A Maven `pom.xml` exports `test` as `direct` with component `scop A `mix.exs` dependency written `{:jason, "~> 1.4", optional: true}` is `direct` with component `scope` `optional` and SPDX `OPTIONAL_DEPENDENCY_OF`. `{:credo, "~> 1.7", only: [:dev, :test], runtime: false}` and `{:dialyxir, "~> 1.4", only: :dev, runtime: false}` are `direct` with component `scope` `excluded` and SPDX `DEV_DEPENDENCY_OF`. The same marks are written when the tuple is split across lines. A development `only:` wins when that tuple also sets `optional: true`. `only: [:dev, :prod]` stays a production row. `optional: false`, a missing `optional`, and `runtime: false` by itself omit component `scope`. A `git:`, `github:`, `path:`, or `in_umbrella:` dependency is absent from the export. A `path:` value or `in_umbrella: true` that points at another Mix project in the same scan is a project reference relative to the scan root and is not a Hex row, so it does not change drift age buckets. The export list has no `mix.lock`, so CycloneDX `dependencies` and SPDX `DEPENDS_ON` are omitted. Drift age buckets count production rows, including optional rows, and leave development rows out. -A `pubspec.yaml` that declares `http` under `dependencies` and `flutter_lints` under `dev_dependencies` exports both as `direct`. `flutter_lints` has component `scope` `excluded` and SPDX `DEV_DEPENDENCY_OF`. `http` omits component `scope`. `flutter: { sdk: flutter }` and `flutter_test: { sdk: flutter }` are absent. A name listed in both sections stays a production row, and the `dependencies` declaration supplies the spec. The export list has no `pubspec.lock`, so CycloneDX `dependencies` and SPDX `DEPENDS_ON` are omitted. Drift age buckets and pub.dev lookups use production rows and leave development rows out. +A `pubspec.yaml` that declares `http` under `dependencies` and `flutter_lints` under `dev_dependencies` exports both as `direct`. `flutter_lints` has component `scope` `excluded` and SPDX `DEV_DEPENDENCY_OF`. `http` omits component `scope`. `flutter: { sdk: flutter }` and `flutter_test: { sdk: flutter }` are absent. A name listed in both sections stays a production row, and the `dependencies` declaration supplies the spec unless `dependency_overrides` replaces it. A package listed only in `dependency_overrides` is a production row. An SDK override is absent and does not replace a declared spec. The export list has no `pubspec.lock`, so CycloneDX `dependencies` and SPDX `DEPENDS_ON` are omitted. Drift age buckets and pub.dev lookups use production rows and leave development rows out. A `composer.json` that declares `monolog/monolog` under `require` and `phpunit/phpunit` under `require-dev` exports both as `direct`. `phpunit/phpunit` has component `scope` `excluded` and SPDX `DEV_DEPENDENCY_OF`. `monolog/monolog` omits component `scope`. A name listed in both tables stays a production row, and the `require` declaration supplies the spec. `php` and `ext-*` platform requirements are not components. CycloneDX `dependencies` and SPDX `DEPENDS_ON` are omitted, including when `composer.lock` is present. Packagist lookups and drift age buckets use production rows and leave development rows out. An unresolved development row is also left out of DriftScore. @@ -171,5 +172,5 @@ A `Gemfile.lock` exports every gem under `GEM` `specs:`, plus a `PATH` or `GIT` 13. A `mix.exs` tuple with a keyword list of any length is a direct row, including a tuple written on more than one line. `only: :dev`, `only: :test`, and a list whose every environment is `:dev` or `:test` are section `devDependencies`. That wins over `optional: true`. `optional: true` otherwise is section `optionalDependencies`. `optional: false`, a missing `optional`, and `runtime: false` stay section `dependencies`. `only: [:dev, :prod]` stays production. A `git:`, `github:`, `path:`, or `in_umbrella:` dependency is absent as a Hex row. A `path:` value or `in_umbrella: true` that points at another Mix project in the same scan is a project reference relative to the scan root. `mix.lock` is not expanded, so a package that appears only there is absent. Drift age buckets and Hex lookups use production rows, including optional rows, and leave development rows out. 14. A .NET `` whose `PrivateAssets` value is `all` (attribute or child element, trimmed, any letter case) is section `devDependencies`. Any other value, and a missing `PrivateAssets`, stays section `dependencies`. The same package referenced again in that project without `PrivateAssets=all` stays section `dependencies`, and the production reference supplies the version. `packages.config` entries stay section `dependencies`. A `` with no `Version` takes the version from the nearest `Directory.Packages.props` `` between the project directory and the scan root. Package ids match without regard to case. The first `` for an id wins. `VersionOverride` wins over that central version and over an inline `Version`. A reference whose version cannot be resolved stays a row with version `unknown`. `packages.lock.json` is expanded: `Direct` and `Transitive` entries are components, and `Project` entries are skipped. The component version is `resolved`. `contentHash` is copied as a SHA-512 digest. The same package id and version under more than one target framework is one component, and package ids are matched without regard to case. The kept spelling is the code-unit minimum of the spellings in the file. A package that appears only in the lockfile is `transitive`. CycloneDX `dependencies` and SPDX `DEPENDS_ON` follow that entry's `dependencies` map when the named package is resolved in the same lockfile. A dependency version that does not match a resolved package is omitted. `Direct` entries are the root dependency list. 15. `Pipfile` `[dev-packages]` is section `devDependencies`. `[packages]` stays section `dependencies`. A name in both tables is stored once, as `dependencies`, and the `[packages]` declaration supplies the spec. A requirements file is section `devDependencies` when its base name has a `dev` or `test` segment split on `-`, `_`, or `.` (`requirements-dev.txt`, `requirements_dev.txt`, `requirements-test.txt`, `dev-requirements.txt`). `requirements.txt` and any other requirements file without that segment stay section `dependencies`. A name declared in one of those development files and in a production manifest row is stored once, as `dependencies`, and the production declaration supplies the spec. Which file is read first does not change that section. `Pipfile.lock` is expanded after `uv.lock`. `default` and `develop` entries that have a `version` are components. A leading `==` is removed before the purl is built (`==2.31.0` is `pkg:pypi/requests@2.31.0`). `hashes` values of the form `sha256:` are digests; any other value is omitted. An `editable` entry and a `path` entry are skipped, including when they also carry a `version`. A git entry with no `version` is skipped. A name that appears only in `develop` is a development row. When the manifest did not declare that version, `vibgrate:scope` stays `transitive`, CycloneDX component `scope` is `excluded`, and SPDX writes `DEV_DEPENDENCY_OF`. A name in both `default` and `develop` stays production, including when the two versions differ and including a PEP 503 spelling difference (`Some_Pkg` and `some-pkg`), so component `scope` is omitted. A name the manifest declared keeps that section: a `[packages]` row stays production even when the lockfile lists that version only under `develop`. The file records no edges, so CycloneDX `dependencies` and SPDX `DEPENDS_ON` are omitted. A truncated or invalid `Pipfile.lock` fails the export. `--no-transitive` omits these lockfile-only rows, including a develop-only package the manifest did not declare. -16. `pubspec.yaml` `dev_dependencies` is section `devDependencies`. `dependencies` stays section `dependencies`. A name in both sections is stored once, as `dependencies`, and the `dependencies` declaration supplies the spec. An SDK entry in either section (`name: { sdk: flutter }`, a nested `sdk:` key, or `sdk: flutter` on the same line) is absent. pub.dev lookups and drift age buckets use production rows and leave development rows out. `pubspec.lock` is not expanded, so CycloneDX `dependencies` and SPDX `DEPENDS_ON` are omitted. A development package declared in the manifest and missing from the lockfile is still a direct row. +16. `pubspec.yaml` `dev_dependencies` is section `devDependencies`. `dependencies` stays section `dependencies`. A name in both sections is stored once, as `dependencies`, and the `dependencies` declaration supplies the spec unless `dependency_overrides` replaces it. `dependency_overrides` is read in the same forms. The override supplies the spec and the row keeps the section of the declaration. A package listed only there is section `dependencies`. An SDK entry in any of the three sections (`name: { sdk: flutter }`, a nested `sdk:` key, or `sdk: flutter` on the same line) is absent, and an SDK override does not replace a declared spec. pub.dev lookups and drift age buckets use production rows and leave development rows out. `pubspec.lock` is not expanded, so CycloneDX `dependencies` and SPDX `DEPENDS_ON` are omitted. When `pubspec.lock` is present its version still supplies `resolvedVersion`. A development package declared in the manifest and missing from the lockfile is still a direct row. 17. `Gemfile.lock` dependency lines under a spec are not edges. A `PATH` or `GIT` gem with `remote: .` is omitted. A platform suffix is not part of the version. A gem that appears only in the lockfile is `transitive`, with purl `pkg:gem/@`. CycloneDX `dependencies` and SPDX `DEPENDS_ON` are omitted. diff --git a/src/core-open/scanners/dart-scanner.ts b/src/core-open/scanners/dart-scanner.ts index 5be91d9..d248a70 100644 --- a/src/core-open/scanners/dart-scanner.ts +++ b/src/core-open/scanners/dart-scanner.ts @@ -142,6 +142,8 @@ interface OpenPubDep { name: string; indent: number; isDev: boolean; + /** Set while the open key is under `dependency_overrides`. */ + isOverride: boolean; version?: string; path?: string; gitUrl?: string; @@ -159,11 +161,33 @@ function pubDepSpec(open: OpenPubDep): string | undefined { return undefined; } -function takeOpenDep(deps: DartDependency[], open: OpenPubDep | null): void { +function takeOpenDep(deps: DartDependency[], overrides: Map, open: OpenPubDep | null): void { if (!open) return; const version = pubDepSpec(open); if (!version) return; - deps.push({ name: open.name, version, isDev: open.isDev }); + if (open.isOverride) overrides.set(open.name, version); + else deps.push({ name: open.name, version, isDev: open.isDev }); +} + +/** + * `dependency_overrides` replaces the spec of a package already collected + * from `dependencies` or `dev_dependencies` and leaves that row's + * production or development flag alone. A name that appears only as an + * override is a production row. Names are appended in code-unit order so + * the row list does not follow map iteration. + */ +function applyDependencyOverrides(deps: DartDependency[], overrides: Map): DartDependency[] { + if (overrides.size === 0) return deps; + const byName = new Map(deps.map((dep) => [dep.name, dep])); + const extras: DartDependency[] = []; + for (const name of [...overrides.keys()].sort(compareCodeUnits)) { + const version = overrides.get(name); + if (!version) continue; + const existing = byName.get(name); + if (existing) existing.version = version; + else extras.push({ name, version, isDev: false }); + } + return extras.length === 0 ? deps : deps.concat(extras); } function flowField(body: string, key: string): string | undefined { @@ -192,35 +216,52 @@ function flowField(body: string, key: string): string | undefined { * flutter_test: * sdk: flutter * mockito: ^5.4.0 + * dependency_overrides: + * http: 1.2.1 * * `dependencies` and `dev_dependencies` are both kept. An SDK entry * (`flutter: { sdk: flutter }`, a nested `sdk: flutter`, or `sdk: flutter` - * on the same line) is skipped in either section. A key with no value at - * the dependency indent opens one package; deeper keys belong to it and - * are never emitted as packages. A name listed in both sections is stored - * once, as a production dependency. + * on the same line) is skipped in either section and under + * `dependency_overrides`. A skipped override does not replace a declared + * spec. A key with no value at the dependency indent opens one package; + * deeper keys belong to it and are never emitted as packages. A name + * listed in both dependency sections is stored once, as a production + * dependency. A `dependency_overrides` entry is read in the same forms + * (one-line version, `{ version: … }` map, nested `version:`, `path:`, + * and `git:`). It replaces that package's spec and keeps the production + * or development flag from the declaration. A package listed only in + * `dependency_overrides` is reported as a production dependency. Overrides + * are applied after both sections are read, so the spec wins whether the + * override is written before or after the declaration. The last override + * for a name wins. `pubspec.lock` is not read here. */ function parsePubspecYaml(content: string): { dartVersion?: string; deps: DartDependency[]; } { const deps: DartDependency[] = []; + const overrides = new Map(); let dartVersion: string | undefined; - let currentSection: 'dependencies' | 'dev_dependencies' | null = null; + let currentSection: 'dependencies' | 'dev_dependencies' | 'dependency_overrides' | null = null; let childIndent: number | null = null; let open: OpenPubDep | null = null; const flush = () => { - takeOpenDep(deps, open); + takeOpenDep(deps, overrides, open); open = null; }; - const enterSection = (section: 'dependencies' | 'dev_dependencies') => { + const enterSection = (section: 'dependencies' | 'dev_dependencies' | 'dependency_overrides') => { flush(); currentSection = section; childIndent = null; }; + const record = (name: string, version: string, isDev: boolean) => { + if (currentSection === 'dependency_overrides') overrides.set(name, version); + else deps.push({ name, version, isDev }); + }; + for (const line of content.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; @@ -228,12 +269,13 @@ function parsePubspecYaml(content: string): { const indent = indentWidth(line); // Extract Dart SDK version. Require a digit so `sdk: flutter` lines - // (SDK dependencies inside dependency sections) cannot clobber the - // environment constraint. Caret and tilde forms (`^3.5.0`, `~3.4.1`) - // count the same as an explicit `>=` range. + // cannot clobber the environment constraint, and ignore `sdk:` inside + // a dependency section, including `dependency_overrides`, so a nested + // SDK constraint is not stored as the Dart version. Caret and tilde + // forms (`^3.5.0`, `~3.4.1`) count the same as an explicit `>=` range. if (trimmed.startsWith('sdk:')) { const match = trimmed.match(/sdk:\s*['"]?>?=?\s*[~^]?(\d[^<'"]*)/); - if (match) dartVersion = match[1]!.trim(); + if (match && currentSection === null) dartVersion = match[1]!.trim(); else if (open && /sdk:\s*flutter\b/.test(trimmed)) open.skip = true; continue; } @@ -246,6 +288,10 @@ function parsePubspecYaml(content: string): { enterSection('dev_dependencies'); continue; } + if (trimmed === 'dependency_overrides:') { + enterSection('dependency_overrides'); + continue; + } // Only an unindented key ends the current section — testing the trimmed // line here would match every indented dependency line and reset the // section before the dependency is captured. @@ -280,7 +326,7 @@ function parsePubspecYaml(content: string): { const name = simpleMatch[1]!; const version = simpleMatch[2]!; if (version === 'sdk:' || trimmed.includes('sdk: flutter')) continue; - deps.push({ name, version, isDev }); + record(name, version, isDev); continue; } @@ -297,12 +343,18 @@ function parsePubspecYaml(content: string): { if (version) spec = version; else if (pathSpec) spec = `path:${pathSpec}`; else if (gitUrl) spec = `git:${gitUrl}`; - if (spec) deps.push({ name, version: spec, isDev }); + if (spec) record(name, spec, isDev); continue; } // Bare key: the following deeper lines belong to this package. - open = { name, indent, isDev, skip: false }; + open = { + name, + indent, + isDev, + isOverride: currentSection === 'dependency_overrides', + skip: false, + }; continue; } @@ -328,7 +380,7 @@ function parsePubspecYaml(content: string): { } flush(); - return { dartVersion, deps: dedupeDartDeps(deps) }; + return { dartVersion, deps: applyDependencyOverrides(dedupeDartDeps(deps), overrides) }; } /** diff --git a/test/dart-pubspec-map.test.ts b/test/dart-pubspec-map.test.ts index d390095..4d15478 100644 --- a/test/dart-pubspec-map.test.ts +++ b/test/dart-pubspec-map.test.ts @@ -22,10 +22,17 @@ const BOGUS = ['version', 'path', 'url', 'ref', 'git']; let root: string; afterEach(() => fs.rmSync(root, { recursive: true, force: true })); -async function scanPubspec(pubspec: string) { +async function scanPubspec( + pubspec: string, + extra?: { lock?: string; manifest?: { pub: Record } }, +) { root = fs.mkdtempSync(path.join(os.tmpdir(), 'vg-dart-pubspec-')); fs.writeFileSync(path.join(root, 'pubspec.yaml'), pubspec); - const projects = await scanDartProjects(root, new PubCache(new Semaphore(1), { pub: {} }, true)); + if (extra?.lock !== undefined) fs.writeFileSync(path.join(root, 'pubspec.lock'), extra.lock); + const projects = await scanDartProjects( + root, + new PubCache(new Semaphore(1), extra?.manifest ?? { pub: {} }, true), + ); return projects[0]!; } @@ -152,3 +159,193 @@ dev_dependencies: } }); }); + +describe('parsePubspecYaml dependency_overrides', () => { + it('uses the override spec for a declared production package', async () => { + const project = await scanPubspec(`name: app +environment: + sdk: ^3.5.0 +dependencies: + http: ^0.13.0 +dependency_overrides: + http: 1.2.1 +`); + expect(project.runtime).toBe('3.5.0'); + expect(specs(project.dependencies)).toEqual({ + http: { section: 'dependencies', currentSpec: '1.2.1' }, + }); + }); + + it('keeps a development flag, reports override-only packages as production, and skips SDK overrides', async () => { + const project = await scanPubspec(`name: app +environment: + sdk: '>=3.5.0 <4.0.0' +dependencies: + http: ^0.13.0 +dev_dependencies: + lints: ^2.0.0 + mockito: ^5.4.0 +dependency_overrides: + http: 1.2.1 + lints: '^3.0.0' + collection: ^1.9.0 + mockito: + sdk: flutter + flutter_test: + sdk: flutter + flutter_inline: { sdk: flutter } + pinned: + sdk: ">=2.19.0 <3.0.0" +`); + expect(project.runtime).toBe('3.5.0'); + expect(specs(project.dependencies)).toEqual({ + http: { section: 'dependencies', currentSpec: '1.2.1' }, + lints: { section: 'devDependencies', currentSpec: "'^3.0.0'" }, + mockito: { section: 'devDependencies', currentSpec: '^5.4.0' }, + collection: { section: 'dependencies', currentSpec: '^1.9.0' }, + }); + for (const name of ['flutter_test', 'flutter_inline', 'pinned', ...BOGUS]) { + expect(project.dependencies.some((dep) => dep.package === name)).toBe(false); + } + }); + + it('applies an override written before the declaration and keeps production when both sections declare it', async () => { + const project = await scanPubspec(`name: app +dependency_overrides: + http: 1.2.1 + shared: + path: ../shared +dev_dependencies: + http: ^0.12.0 +dependencies: + http: ^0.13.0 +`); + expect(specs(project.dependencies)).toEqual({ + http: { section: 'dependencies', currentSpec: '1.2.1' }, + shared: { section: 'dependencies', currentSpec: 'path:../shared' }, + }); + }); + + it('reads map, path, and git overrides in the same forms as dependencies', async () => { + const project = await scanPubspec(`name: app +dependencies: + http: ^0.13.0 + crypto: ^2.0.0 +dependency_overrides: + http: { version: 1.2.1 } + crypto: + hosted: + name: crypto + url: https://pub.example.com + version: ^3.0.0 + kittens: + git: https://github.com/munificent/kittens.git + analyzer_plugin: + git: + url: https://github.com/example/plugin.git + ref: main + local: + path: "../pkgs/local" +`); + expect(specs(project.dependencies)).toEqual({ + analyzer_plugin: { + section: 'dependencies', + currentSpec: 'git:https://github.com/example/plugin.git#main', + }, + crypto: { section: 'dependencies', currentSpec: '^3.0.0' }, + http: { section: 'dependencies', currentSpec: '1.2.1' }, + kittens: { + section: 'dependencies', + currentSpec: 'git:https://github.com/munificent/kittens.git', + }, + local: { section: 'dependencies', currentSpec: 'path:../pkgs/local' }, + }); + for (const name of ['hosted', 'name', ...BOGUS]) { + expect(project.dependencies.some((dep) => dep.package === name)).toBe(false); + } + }); + + it('matches a pubspec that has no dependency_overrides section', async () => { + const project = await scanPubspec(`name: app +environment: + sdk: '>=3.0.0 <4.0.0' +dependencies: + flutter: + sdk: flutter + provider: ^6.1.0 + http: '^1.2.0' +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^5.4.0 +flutter: + uses-material-design: true +`); + expect(project.runtime).toBe('3.0.0'); + expect(specs(project.dependencies)).toEqual({ + http: { section: 'dependencies', currentSpec: "'^1.2.0'" }, + mockito: { section: 'devDependencies', currentSpec: '^5.4.0' }, + provider: { section: 'dependencies', currentSpec: '^6.1.0' }, + }); + }); + + it('keeps the pubspec.lock resolved version when an override changes the spec', async () => { + const project = await scanPubspec( + `name: app +dependencies: + http: ^0.13.0 +dev_dependencies: + lints: ^2.0.0 +dependency_overrides: + http: 1.2.1 + lints: ^3.0.0 + collection: 1.9.0 +`, + { + lock: `packages: + http: + dependency: "direct main" + version: "1.2.2" + lints: + dependency: "direct dev" + version: "2.1.0" + collection: + dependency: "direct main" + version: "1.9.1" +`, + manifest: { + pub: { + http: { latest: '2.0.0', versions: ['1.2.2', '2.0.0'] }, + collection: { latest: '1.9.1', versions: ['1.9.0', '1.9.1'] }, + lints: { latest: '3.0.0', versions: ['2.1.0', '3.0.0'] }, + }, + }, + }, + ); + const byPackage = new Map(project.dependencies.map((dep) => [dep.package, dep])); + expect(byPackage.get('http')).toMatchObject({ + section: 'dependencies', + currentSpec: '1.2.1', + resolvedVersion: '1.2.2', + latestStable: '2.0.0', + majorsBehind: 1, + drift: 'major-behind', + }); + expect(byPackage.get('lints')).toMatchObject({ + section: 'devDependencies', + currentSpec: '^3.0.0', + resolvedVersion: '2.1.0', + latestStable: null, + majorsBehind: null, + drift: 'unknown', + }); + expect(byPackage.get('collection')).toMatchObject({ + section: 'dependencies', + currentSpec: '1.9.0', + resolvedVersion: '1.9.1', + latestStable: '1.9.1', + majorsBehind: 0, + drift: 'current', + }); + }); +});