Merge branch 'main' into merge-sse-package
diff --git a/.github/ISSUE_TEMPLATE/package_config.md b/.github/ISSUE_TEMPLATE/package_config.md
new file mode 100644
index 0000000..f6322d0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/package_config.md
@@ -0,0 +1,5 @@
+---
+name: "package:package_config"
+about: "Create a bug or file a feature request against package:package_config."
+labels: "package:package_config"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/pool.md b/.github/ISSUE_TEMPLATE/pool.md
new file mode 100644
index 0000000..7af32c4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/pool.md
@@ -0,0 +1,5 @@
+---
+name: "package:pool"
+about: "Create a bug or file a feature request against package:pool."
+labels: "package:pool"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/pub_semver.md b/.github/ISSUE_TEMPLATE/pub_semver.md
new file mode 100644
index 0000000..c7db9b5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/pub_semver.md
@@ -0,0 +1,5 @@
+---
+name: "package:pub_semver"
+about: "Create a bug or file a feature request against package:pub_semver."
+labels: "package:pub_semver"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/source_maps.md b/.github/ISSUE_TEMPLATE/source_maps.md
new file mode 100644
index 0000000..a1e390a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/source_maps.md
@@ -0,0 +1,5 @@
+---
+name: "package:source_maps"
+about: "Create a bug or file a feature request against package:source_maps."
+labels: "package:source_maps"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/source_span.md b/.github/ISSUE_TEMPLATE/source_span.md
new file mode 100644
index 0000000..7dbb3c4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/source_span.md
@@ -0,0 +1,5 @@
+---
+name: "package:source_span"
+about: "Create a bug or file a feature request against package:source_span."
+labels: "package:source_span"
+---
\ No newline at end of file
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 858a9e7..1cc4b20 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -80,10 +80,30 @@
- changed-files:
- any-glob-to-any-file: 'pkgs/oauth2/**'
+'package:package_config':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/package_config/**'
+
+'package:pool':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/pool/**'
+
+'package:pub_semver':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/pub_semver/**'
+
'package:source_map_stack_trace':
- changed-files:
- any-glob-to-any-file: 'pkgs/source_map_stack_trace/**'
+'package:source_maps':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/source_maps/**'
+
+'package:source_span':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/source_span/**'
+
'package:sse':
- changed-files:
- any-glob-to-any-file: 'pkgs/sse/**'
diff --git a/.github/workflows/package_config.yaml b/.github/workflows/package_config.yaml
new file mode 100644
index 0000000..416ea1a
--- /dev/null
+++ b/.github/workflows/package_config.yaml
@@ -0,0 +1,71 @@
+name: package:package_config
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/package_config.yml'
+ - 'pkgs/package_config/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/package_config.yml'
+ - 'pkgs/package_config/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+ run:
+ working-directory: pkgs/package_config/
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ sdk: [3.4, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run tests
+ run: dart test -p chrome,vm
+ if: always() && steps.install.outcome == 'success'
diff --git a/.github/workflows/pool.yaml b/.github/workflows/pool.yaml
new file mode 100644
index 0000000..6d64062
--- /dev/null
+++ b/.github/workflows/pool.yaml
@@ -0,0 +1,78 @@
+name: package:pool
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/pool.yaml'
+ - 'pkgs/pool/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/pool.yaml'
+ - 'pkgs/pool/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+ run:
+ working-directory: pkgs/pool/
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [3.4, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
+ - name: Run Chrome tests
+ run: dart test --platform chrome
+ if: always() && steps.install.outcome == 'success'
+ - name: Run Chrome tests - wasm
+ run: dart test --platform chrome -c dart2wasm
+ if: always() && steps.install.outcome == 'success' && matrix.sdk == 'dev'
diff --git a/.github/workflows/pub_semver.yaml b/.github/workflows/pub_semver.yaml
new file mode 100644
index 0000000..ba0db18
--- /dev/null
+++ b/.github/workflows/pub_semver.yaml
@@ -0,0 +1,75 @@
+name: package:pub_semver
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/pub_semver.yaml'
+ - 'pkgs/pub_semver/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/pub_semver.yaml'
+ - 'pkgs/pub_semver/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+ run:
+ working-directory: pkgs/pub_semver/
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [3.4, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
+ - name: Run Chrome tests
+ run: dart test --platform chrome --compiler dart2js,dart2wasm
+ if: always() && steps.install.outcome == 'success'
diff --git a/.github/workflows/source_maps.yaml b/.github/workflows/source_maps.yaml
new file mode 100644
index 0000000..2ae0f20
--- /dev/null
+++ b/.github/workflows/source_maps.yaml
@@ -0,0 +1,72 @@
+name: package:source_maps
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/source_maps.yaml'
+ - 'pkgs/source_maps/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/source_maps.yaml'
+ - 'pkgs/source_maps/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+ run:
+ working-directory: pkgs/source_maps/
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [3.3.0, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
diff --git a/.github/workflows/source_span.yaml b/.github/workflows/source_span.yaml
new file mode 100644
index 0000000..2c2ba05
--- /dev/null
+++ b/.github/workflows/source_span.yaml
@@ -0,0 +1,75 @@
+name: package:source_span
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/source_span.yml'
+ - 'pkgs/source_span/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/source_span.yml'
+ - 'pkgs/source_span/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+ run:
+ working-directory: pkgs/source_span/
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [3.1.0, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
+ - name: Run Chrome tests
+ run: dart test --platform chrome
+ if: always() && steps.install.outcome == 'success'
diff --git a/README.md b/README.md
index a941dec..79d1dde 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,12 @@
| [json_rpc_2](pkgs/json_rpc_2/) | Utilities to write a client or server using the JSON-RPC 2.0 spec. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Ajson_rpc_2) | [](https://pub.dev/packages/json_rpc_2) |
| [mime](pkgs/mime/) | Utilities for handling media (MIME) types, including determining a type from a file extension and file contents. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Amime) | [](https://pub.dev/packages/mime) |
| [oauth2](pkgs/oauth2/) | A client library for authenticating with a remote service via OAuth2 on behalf of a user, and making authorized HTTP requests with the user's OAuth2 credentials. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aoauth2) | [](https://pub.dev/packages/oauth2) |
+| [package_config](pkgs/package_config/) | Support for reading and writing Dart Package Configuration files. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apackage_config) | [](https://pub.dev/packages/package_config) |
+| [pool](pkgs/pool/) | Manage a finite pool of resources. Useful for controlling concurrent file system or network requests. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apool) | [](https://pub.dev/packages/pool) |
+| [pub_semver](pkgs/pub_semver/) | Versions and version constraints implementing pub's versioning policy. This is very similar to vanilla semver, with a few corner cases. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apub_semver) | [](https://pub.dev/packages/pub_semver) |
| [source_map_stack_trace](pkgs/source_map_stack_trace/) | A package for applying source maps to stack traces. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_map_stack_trace) | [](https://pub.dev/packages/source_map_stack_trace) |
+| [source_maps](pkgs/source_maps/) | A library to programmatically manipulate source map files. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_maps) | [](https://pub.dev/packages/source_maps) |
+| [source_span](pkgs/source_span/) | Provides a standard representation for source code locations and spans. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_span) | [](https://pub.dev/packages/source_span) |
| [sse](pkgs/sse/) | Provides client and server functionality for setting up bi-directional communication through Server Sent Events (SSE) and corresponding POST requests. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asse) | [](https://pub.dev/packages/sse) |
| [unified_analytics](pkgs/unified_analytics/) | A package for logging analytics for all Dart and Flutter related tooling to Google Analytics. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aunified_analytics) | [](https://pub.dev/packages/unified_analytics) |
diff --git a/pkgs/package_config/.gitignore b/pkgs/package_config/.gitignore
new file mode 100644
index 0000000..7b888b8
--- /dev/null
+++ b/pkgs/package_config/.gitignore
@@ -0,0 +1,7 @@
+.packages
+.pub
+.dart_tool/
+.vscode/
+packages
+pubspec.lock
+doc/api/
diff --git a/pkgs/package_config/AUTHORS b/pkgs/package_config/AUTHORS
new file mode 100644
index 0000000..e8063a8
--- /dev/null
+++ b/pkgs/package_config/AUTHORS
@@ -0,0 +1,6 @@
+# Below is a list of people and organizations that have contributed
+# to the project. Names should be added to the list like so:
+#
+# Name/Organization <email address>
+
+Google Inc.
diff --git a/pkgs/package_config/CHANGELOG.md b/pkgs/package_config/CHANGELOG.md
new file mode 100644
index 0000000..101a0fe
--- /dev/null
+++ b/pkgs/package_config/CHANGELOG.md
@@ -0,0 +1,108 @@
+## 2.1.1
+
+- Require Dart 3.4
+- Move to `dart-lang/tools` monorepo.
+
+## 2.1.0
+
+- Adds `minVersion` to `findPackageConfig` and `findPackageConfigVersion`
+ which allows ignoring earlier versions (which currently only means
+ ignoring version 1, aka. `.packages` files.)
+
+- Changes the version number of `SimplePackageConfig.empty` to the
+ current maximum version.
+
+- Improve file read performance; improve lookup performance.
+- Emit an error when a package is inside the package root of another package.
+- Fix a link in the readme.
+
+## 2.0.2
+
+- Update package description and README.
+- Change to package:lints for style checking.
+- Add an example.
+
+## 2.0.1
+
+- Use unique library names to correct docs issue.
+
+## 2.0.0
+
+- Migrate to null safety.
+- Remove legacy APIs.
+- Adds `relativeRoot` property to `Package` which controls whether to
+ make the root URI relative when writing a configuration file.
+
+## 1.9.3
+
+- Fix `Package` constructor not accepting relative `packageUriRoot`.
+
+## 1.9.2
+
+- Updated to support new rules for picking `package_config.json` over
+ a specified `.packages`.
+- Deduce package root from `.packages` derived package configuration,
+ and default all such packages to language version 2.7.
+
+## 1.9.1
+
+- Remove accidental transitive import of `dart:io` from entrypoints that are
+ supposed to be cross-platform compatible.
+
+## 1.9.0
+
+- Based on new JSON file format with more content.
+- This version includes all the new functionality intended for a 2.0.0
+ version, as well as the, now deprecated, version 1 functionality.
+ When we release 2.0.0, the deprecated functionality will be removed.
+
+## 1.1.0
+
+- Allow parsing files with default-package entries and metadata.
+ A default-package entry has an empty key and a valid package name
+ as value.
+ Metadata is attached as fragments to base URIs.
+
+## 1.0.5
+
+- Fix usage of SDK constants.
+
+## 1.0.4
+
+- Set max SDK version to <3.0.0.
+
+## 1.0.3
+
+- Removed unneeded dependency constraint on SDK.
+
+## 1.0.2
+
+- Update SDK constraint to be 2.0.0 dev friendly.
+
+## 1.0.1
+
+- Fix test to not write to sink after it's closed.
+
+## 1.0.0
+
+- Public API marked stable.
+
+## 0.1.5
+
+- `FilePackagesDirectoryPackages.getBase(..)` performance improvements.
+
+## 0.1.4
+
+- Strong mode fixes.
+
+## 0.1.3
+
+- Invalid test cleanup (to keep up with changes in `Uri`).
+
+## 0.1.1
+
+- Syntax updates.
+
+## 0.1.0
+
+- Initial implementation.
diff --git a/pkgs/package_config/LICENSE b/pkgs/package_config/LICENSE
new file mode 100644
index 0000000..7670007
--- /dev/null
+++ b/pkgs/package_config/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2019, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/package_config/README.md b/pkgs/package_config/README.md
new file mode 100644
index 0000000..76fd3cb
--- /dev/null
+++ b/pkgs/package_config/README.md
@@ -0,0 +1,26 @@
+[](https://github.com/dart-lang/tools/actions/workflows/package_config.yaml)
+[](https://pub.dev/packages/package_config)
+[](https://pub.dev/packages/package_config/publisher)
+
+Support for working with **Package Configuration** files as described
+in the Package Configuration v2 [design document](https://github.com/dart-lang/language/blob/master/accepted/2.8/language-versioning/package-config-file-v2.md).
+
+A Dart package configuration file is used to resolve Dart package names (e.g.
+`foobar`) to Dart files containing the source code for that package (e.g.
+`file:///Users/myuser/.pub-cache/hosted/pub.dartlang.org/foobar-1.1.0`). The
+standard package configuration file is `.dart_tool/package_config.json`, and is
+written by the Dart tool when the command `dart pub get` is run.
+
+The primary libraries of this package are
+* `package_config.dart`:
+ Defines the `PackageConfig` class and other types needed to use
+ package configurations, and provides functions to find, read and
+ write package configuration files.
+
+* `package_config_types.dart`:
+ Just the `PackageConfig` class and other types needed to use
+ package configurations. This library does not depend on `dart:io`.
+
+The package includes deprecated backwards compatible functionality to
+work with the `.packages` file. This functionality will not be maintained,
+and will be removed in a future version of this package.
diff --git a/pkgs/package_config/analysis_options.yaml b/pkgs/package_config/analysis_options.yaml
new file mode 100644
index 0000000..c0249e5
--- /dev/null
+++ b/pkgs/package_config/analysis_options.yaml
@@ -0,0 +1,5 @@
+# Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+include: package:dart_flutter_team_lints/analysis_options.yaml
diff --git a/pkgs/package_config/example/main.dart b/pkgs/package_config/example/main.dart
new file mode 100644
index 0000000..db137ca
--- /dev/null
+++ b/pkgs/package_config/example/main.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io' show Directory;
+
+import 'package:package_config/package_config.dart';
+
+void main() async {
+ var packageConfig = await findPackageConfig(Directory.current);
+ if (packageConfig == null) {
+ print('Failed to locate or read package config.');
+ } else {
+ print('This package depends on ${packageConfig.packages.length} packages:');
+ for (var package in packageConfig.packages) {
+ print('- ${package.name}');
+ }
+ }
+}
diff --git a/pkgs/package_config/lib/package_config.dart b/pkgs/package_config/lib/package_config.dart
new file mode 100644
index 0000000..074c977
--- /dev/null
+++ b/pkgs/package_config/lib/package_config.dart
@@ -0,0 +1,199 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A package configuration is a way to assign file paths to package URIs,
+/// and vice-versa.
+///
+/// This package provides functionality to find, read and write package
+/// configurations in the [specified format](https://github.com/dart-lang/language/blob/master/accepted/future-releases/language-versioning/package-config-file-v2.md).
+library;
+
+import 'dart:io' show Directory, File;
+import 'dart:typed_data' show Uint8List;
+
+import 'src/discovery.dart' as discover;
+import 'src/errors.dart' show throwError;
+import 'src/package_config.dart';
+import 'src/package_config_io.dart';
+
+export 'package_config_types.dart';
+
+/// Reads a specific package configuration file.
+///
+/// The file must exist and be readable.
+/// It must be either a valid `package_config.json` file
+/// or a valid `.packages` file.
+/// It is considered a `package_config.json` file if its first character
+/// is a `{`.
+///
+/// If the file is a `.packages` file (the file name is `.packages`)
+/// and [preferNewest] is true, the default, also checks if there is
+/// a `.dart_tool/package_config.json` file next
+/// to the original file, and if so, loads that instead.
+/// If [preferNewest] is set to false, a directly specified `.packages` file
+/// is loaded even if there is an available `package_config.json` file.
+/// The caller can determine this from the [PackageConfig.version]
+/// being 1 and look for a `package_config.json` file themselves.
+///
+/// If [onError] is provided, the configuration file parsing will report errors
+/// by calling that function, and then try to recover.
+/// The returned package configuration is a *best effort* attempt to create
+/// a valid configuration from the invalid configuration file.
+/// If no [onError] is provided, errors are thrown immediately.
+Future<PackageConfig> loadPackageConfig(File file,
+ {bool preferNewest = true, void Function(Object error)? onError}) =>
+ readAnyConfigFile(file, preferNewest, onError ?? throwError);
+
+/// Reads a specific package configuration URI.
+///
+/// The file of the URI must exist and be readable.
+/// It must be either a valid `package_config.json` file
+/// or a valid `.packages` file.
+/// It is considered a `package_config.json` file if its first
+/// non-whitespace character is a `{`.
+///
+/// If [preferNewest] is true, the default, and the file is a `.packages` file,
+/// as determined by its file name being `.packages`,
+/// first checks if there is a `.dart_tool/package_config.json` file
+/// next to the original file, and if so, loads that instead.
+/// The [file] *must not* be a `package:` URI.
+/// If [preferNewest] is set to false, a directly specified `.packages` file
+/// is loaded even if there is an available `package_config.json` file.
+/// The caller can determine this from the [PackageConfig.version]
+/// being 1 and look for a `package_config.json` file themselves.
+///
+/// If [loader] is provided, URIs are loaded using that function.
+/// The future returned by the loader must complete with a [Uint8List]
+/// containing the entire file content encoded as UTF-8,
+/// or with `null` if the file does not exist.
+/// The loader may throw at its own discretion, for situations where
+/// it determines that an error might be need user attention,
+/// but it is always allowed to return `null`.
+/// This function makes no attempt to catch such errors.
+/// As such, it may throw any error that [loader] throws.
+///
+/// If no [loader] is supplied, a default loader is used which
+/// only accepts `file:`, `http:` and `https:` URIs,
+/// and which uses the platform file system and HTTP requests to
+/// fetch file content. The default loader never throws because
+/// of an I/O issue, as long as the location URIs are valid.
+/// As such, it does not distinguish between a file not existing,
+/// and it being temporarily locked or unreachable.
+///
+/// If [onError] is provided, the configuration file parsing will report errors
+/// by calling that function, and then try to recover.
+/// The returned package configuration is a *best effort* attempt to create
+/// a valid configuration from the invalid configuration file.
+/// If no [onError] is provided, errors are thrown immediately.
+Future<PackageConfig> loadPackageConfigUri(Uri file,
+ {Future<Uint8List?> Function(Uri uri)? loader,
+ bool preferNewest = true,
+ void Function(Object error)? onError}) =>
+ readAnyConfigFileUri(file, loader, onError ?? throwError, preferNewest);
+
+/// Finds a package configuration relative to [directory].
+///
+/// If [directory] contains a package configuration,
+/// either a `.dart_tool/package_config.json` file or,
+/// if not, a `.packages`, then that file is loaded.
+///
+/// If no file is found in the current directory,
+/// then the parent directories are checked recursively,
+/// all the way to the root directory, to check if those contains
+/// a package configuration.
+/// If [recurse] is set to `false`, this parent directory check is not
+/// performed.
+///
+/// If [onError] is provided, the configuration file parsing will report errors
+/// by calling that function, and then try to recover.
+/// The returned package configuration is a *best effort* attempt to create
+/// a valid configuration from the invalid configuration file.
+/// If no [onError] is provided, errors are thrown immediately.
+///
+/// If [minVersion] is set to something greater than its default,
+/// any lower-version configuration files are ignored in the search.
+///
+/// Returns `null` if no configuration file is found.
+Future<PackageConfig?> findPackageConfig(Directory directory,
+ {bool recurse = true,
+ void Function(Object error)? onError,
+ int minVersion = 1}) {
+ if (minVersion > PackageConfig.maxVersion) {
+ throw ArgumentError.value(minVersion, 'minVersion',
+ 'Maximum known version is ${PackageConfig.maxVersion}');
+ }
+ return discover.findPackageConfig(
+ directory, minVersion, recurse, onError ?? throwError);
+}
+
+/// Finds a package configuration relative to [location].
+///
+/// If [location] contains a package configuration,
+/// either a `.dart_tool/package_config.json` file or,
+/// if not, a `.packages`, then that file is loaded.
+/// The [location] URI *must not* be a `package:` URI.
+/// It should be a hierarchical URI which is supported
+/// by [loader].
+///
+/// If no file is found in the current directory,
+/// then the parent directories are checked recursively,
+/// all the way to the root directory, to check if those contains
+/// a package configuration.
+/// If [recurse] is set to `false`, this parent directory check is not
+/// performed.
+///
+/// If [loader] is provided, URIs are loaded using that function.
+/// The future returned by the loader must complete with a [Uint8List]
+/// containing the entire file content,
+/// or with `null` if the file does not exist.
+/// The loader may throw at its own discretion, for situations where
+/// it determines that an error might be need user attention,
+/// but it is always allowed to return `null`.
+/// This function makes no attempt to catch such errors.
+///
+/// If no [loader] is supplied, a default loader is used which
+/// only accepts `file:`, `http:` and `https:` URIs,
+/// and which uses the platform file system and HTTP requests to
+/// fetch file content. The default loader never throws because
+/// of an I/O issue, as long as the location URIs are valid.
+/// As such, it does not distinguish between a file not existing,
+/// and it being temporarily locked or unreachable.
+///
+/// If [onError] is provided, the configuration file parsing will report errors
+/// by calling that function, and then try to recover.
+/// The returned package configuration is a *best effort* attempt to create
+/// a valid configuration from the invalid configuration file.
+/// If no [onError] is provided, errors are thrown immediately.
+///
+/// If [minVersion] is set to something greater than its default,
+/// any lower-version configuration files are ignored in the search.
+///
+/// Returns `null` if no configuration file is found.
+Future<PackageConfig?> findPackageConfigUri(Uri location,
+ {bool recurse = true,
+ int minVersion = 1,
+ Future<Uint8List?> Function(Uri uri)? loader,
+ void Function(Object error)? onError}) {
+ if (minVersion > PackageConfig.maxVersion) {
+ throw ArgumentError.value(minVersion, 'minVersion',
+ 'Maximum known version is ${PackageConfig.maxVersion}');
+ }
+ return discover.findPackageConfigUri(
+ location, minVersion, loader, onError ?? throwError, recurse);
+}
+
+/// Writes a package configuration to the provided directory.
+///
+/// Writes `.dart_tool/package_config.json` relative to [directory].
+/// If the `.dart_tool/` directory does not exist, it is created.
+/// If it cannot be created, this operation fails.
+///
+/// Also writes a `.packages` file in [directory].
+/// This will stop happening eventually as the `.packages` file becomes
+/// discontinued.
+/// A comment is generated if `[PackageConfig.extraData]` contains a
+/// `"generator"` entry.
+Future<void> savePackageConfig(
+ PackageConfig configuration, Directory directory) =>
+ writePackageConfigJsonFile(configuration, directory);
diff --git a/pkgs/package_config/lib/package_config_types.dart b/pkgs/package_config/lib/package_config_types.dart
new file mode 100644
index 0000000..825f7ac
--- /dev/null
+++ b/pkgs/package_config/lib/package_config_types.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A package configuration is a way to assign file paths to package URIs,
+/// and vice-versa.
+///
+/// {@canonicalFor package_config.InvalidLanguageVersion}
+/// {@canonicalFor package_config.LanguageVersion}
+/// {@canonicalFor package_config.Package}
+/// {@canonicalFor package_config.PackageConfig}
+/// {@canonicalFor errors.PackageConfigError}
+library;
+
+export 'src/errors.dart' show PackageConfigError;
+export 'src/package_config.dart'
+ show InvalidLanguageVersion, LanguageVersion, Package, PackageConfig;
diff --git a/pkgs/package_config/lib/src/discovery.dart b/pkgs/package_config/lib/src/discovery.dart
new file mode 100644
index 0000000..b678410
--- /dev/null
+++ b/pkgs/package_config/lib/src/discovery.dart
@@ -0,0 +1,148 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'errors.dart';
+import 'package_config_impl.dart';
+import 'package_config_io.dart';
+import 'package_config_json.dart';
+import 'packages_file.dart' as packages_file;
+import 'util_io.dart' show defaultLoader, pathJoin;
+
+final Uri packageConfigJsonPath = Uri(path: '.dart_tool/package_config.json');
+final Uri dotPackagesPath = Uri(path: '.packages');
+final Uri currentPath = Uri(path: '.');
+final Uri parentPath = Uri(path: '..');
+
+/// Discover the package configuration for a Dart script.
+///
+/// The [baseDirectory] points to the directory of the Dart script.
+/// A package resolution strategy is found by going through the following steps,
+/// and stopping when something is found.
+///
+/// * Check if a `.dart_tool/package_config.json` file exists in the directory.
+/// * Check if a `.packages` file exists in the directory
+/// (if `minVersion <= 1`).
+/// * Repeat these checks for the parent directories until reaching the
+/// root directory if [recursive] is true.
+///
+/// If any of these tests succeed, a `PackageConfig` class is returned.
+/// Returns `null` if no configuration was found. If a configuration
+/// is needed, then the caller can supply [PackageConfig.empty].
+///
+/// If [minVersion] is greater than 1, `.packages` files are ignored.
+/// If [minVersion] is greater than the version read from the
+/// `package_config.json` file, it too is ignored.
+Future<PackageConfig?> findPackageConfig(Directory baseDirectory,
+ int minVersion, bool recursive, void Function(Object error) onError) async {
+ var directory = baseDirectory;
+ if (!directory.isAbsolute) directory = directory.absolute;
+ if (!await directory.exists()) {
+ return null;
+ }
+ do {
+ // Check for $cwd/.packages
+ var packageConfig =
+ await findPackageConfigInDirectory(directory, minVersion, onError);
+ if (packageConfig != null) return packageConfig;
+ if (!recursive) break;
+ // Check in parent directories.
+ var parentDirectory = directory.parent;
+ if (parentDirectory.path == directory.path) break;
+ directory = parentDirectory;
+ } while (true);
+ return null;
+}
+
+/// Similar to [findPackageConfig] but based on a URI.
+Future<PackageConfig?> findPackageConfigUri(
+ Uri location,
+ int minVersion,
+ Future<Uint8List?> Function(Uri uri)? loader,
+ void Function(Object error) onError,
+ bool recursive) async {
+ if (location.isScheme('package')) {
+ onError(PackageConfigArgumentError(
+ location, 'location', 'Must not be a package: URI'));
+ return null;
+ }
+ if (loader == null) {
+ if (location.isScheme('file')) {
+ return findPackageConfig(
+ Directory.fromUri(location.resolveUri(currentPath)),
+ minVersion,
+ recursive,
+ onError);
+ }
+ loader = defaultLoader;
+ }
+ if (!location.path.endsWith('/')) location = location.resolveUri(currentPath);
+ while (true) {
+ var file = location.resolveUri(packageConfigJsonPath);
+ var bytes = await loader(file);
+ if (bytes != null) {
+ var config = parsePackageConfigBytes(bytes, file, onError);
+ if (config.version >= minVersion) return config;
+ }
+ if (minVersion <= 1) {
+ file = location.resolveUri(dotPackagesPath);
+ bytes = await loader(file);
+ if (bytes != null) {
+ return packages_file.parse(bytes, file, onError);
+ }
+ }
+ if (!recursive) break;
+ var parent = location.resolveUri(parentPath);
+ if (parent == location) break;
+ location = parent;
+ }
+ return null;
+}
+
+/// Finds a `.packages` or `.dart_tool/package_config.json` file in [directory].
+///
+/// Loads the file, if it is there, and returns the resulting [PackageConfig].
+/// Returns `null` if the file isn't there.
+/// Reports a [FormatException] if a file is there but the content is not valid.
+/// If the file exists, but fails to be read, the file system error is reported.
+///
+/// If [onError] is supplied, parsing errors are reported using that, and
+/// a best-effort attempt is made to return a package configuration.
+/// This may be the empty package configuration.
+///
+/// If [minVersion] is greater than 1, `.packages` files are ignored.
+/// If [minVersion] is greater than the version read from the
+/// `package_config.json` file, it too is ignored.
+Future<PackageConfig?> findPackageConfigInDirectory(Directory directory,
+ int minVersion, void Function(Object error) onError) async {
+ var packageConfigFile = await checkForPackageConfigJsonFile(directory);
+ if (packageConfigFile != null) {
+ var config = await readPackageConfigJsonFile(packageConfigFile, onError);
+ if (config.version < minVersion) return null;
+ return config;
+ }
+ if (minVersion <= 1) {
+ packageConfigFile = await checkForDotPackagesFile(directory);
+ if (packageConfigFile != null) {
+ return await readDotPackagesFile(packageConfigFile, onError);
+ }
+ }
+ return null;
+}
+
+Future<File?> checkForPackageConfigJsonFile(Directory directory) async {
+ assert(directory.isAbsolute);
+ var file =
+ File(pathJoin(directory.path, '.dart_tool', 'package_config.json'));
+ if (await file.exists()) return file;
+ return null;
+}
+
+Future<File?> checkForDotPackagesFile(Directory directory) async {
+ var file = File(pathJoin(directory.path, '.packages'));
+ if (await file.exists()) return file;
+ return null;
+}
diff --git a/pkgs/package_config/lib/src/errors.dart b/pkgs/package_config/lib/src/errors.dart
new file mode 100644
index 0000000..a66fef7
--- /dev/null
+++ b/pkgs/package_config/lib/src/errors.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// General superclass of most errors and exceptions thrown by this package.
+///
+/// Only covers errors thrown while parsing package configuration files.
+/// Programming errors and I/O exceptions are not covered.
+abstract class PackageConfigError {
+ PackageConfigError._();
+}
+
+class PackageConfigArgumentError extends ArgumentError
+ implements PackageConfigError {
+ PackageConfigArgumentError(
+ Object? super.value, String super.name, String super.message)
+ : super.value();
+
+ PackageConfigArgumentError.from(ArgumentError error)
+ : super.value(error.invalidValue, error.name, error.message);
+}
+
+class PackageConfigFormatException extends FormatException
+ implements PackageConfigError {
+ PackageConfigFormatException(super.message, Object? super.source,
+ [super.offset]);
+
+ PackageConfigFormatException.from(FormatException exception)
+ : super(exception.message, exception.source, exception.offset);
+}
+
+/// The default `onError` handler.
+// ignore: only_throw_errors
+Never throwError(Object error) => throw error;
diff --git a/pkgs/package_config/lib/src/package_config.dart b/pkgs/package_config/lib/src/package_config.dart
new file mode 100644
index 0000000..155dfc5
--- /dev/null
+++ b/pkgs/package_config/lib/src/package_config.dart
@@ -0,0 +1,402 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:typed_data';
+
+import 'errors.dart';
+import 'package_config_impl.dart';
+import 'package_config_json.dart';
+
+/// A package configuration.
+///
+/// Associates configuration data to packages and files in packages.
+///
+/// More members may be added to this class in the future,
+/// so classes outside of this package must not implement [PackageConfig]
+/// or any subclass of it.
+abstract class PackageConfig {
+ /// The largest configuration version currently recognized.
+ static const int maxVersion = 2;
+
+ /// An empty package configuration.
+ ///
+ /// A package configuration with no available packages.
+ /// Is used as a default value where a package configuration
+ /// is expected, but none have been specified or found.
+ static const PackageConfig empty = SimplePackageConfig.empty();
+
+ /// Creates a package configuration with the provided available [packages].
+ ///
+ /// The packages must be valid packages (valid package name, valid
+ /// absolute directory URIs, valid language version, if any),
+ /// and there must not be two packages with the same name.
+ ///
+ /// The package's root ([Package.root]) and package-root
+ /// ([Package.packageUriRoot]) paths must satisfy a number of constraints
+ /// We say that one path (which we know ends with a `/` character)
+ /// is inside another path, if the latter path is a prefix of the former path,
+ /// including the two paths being the same.
+ ///
+ /// * No package's root must be the same as another package's root.
+ /// * The package-root of a package must be inside the package's root.
+ /// * If one package's package-root is inside another package's root,
+ /// then the latter package's package root must not be inside the former
+ /// package's root. (No getting between a package and its package root!)
+ /// This also disallows a package's root being the same as another
+ /// package's package root.
+ ///
+ /// If supplied, the [extraData] will be available as the
+ /// [PackageConfig.extraData] of the created configuration.
+ ///
+ /// The version of the resulting configuration is always [maxVersion].
+ factory PackageConfig(Iterable<Package> packages, {Object? extraData}) =>
+ SimplePackageConfig(maxVersion, packages, extraData);
+
+ /// Parses a package configuration file.
+ ///
+ /// The [bytes] must be an UTF-8 encoded JSON object
+ /// containing a valid package configuration.
+ ///
+ /// The [baseUri] is used as the base for resolving relative
+ /// URI references in the configuration file. If the configuration
+ /// has been read from a file, the [baseUri] can be the URI of that
+ /// file, or of the directory it occurs in.
+ ///
+ /// If [onError] is provided, errors found during parsing or building
+ /// the configuration are reported by calling [onError] instead of
+ /// throwing, and parser makes a *best effort* attempt to continue
+ /// despite the error. The input must still be valid JSON.
+ /// The result may be [PackageConfig.empty] if there is no way to
+ /// extract useful information from the bytes.
+ static PackageConfig parseBytes(Uint8List bytes, Uri baseUri,
+ {void Function(Object error)? onError}) =>
+ parsePackageConfigBytes(bytes, baseUri, onError ?? throwError);
+
+ /// Parses a package configuration file.
+ ///
+ /// The [configuration] must be a JSON object
+ /// containing a valid package configuration.
+ ///
+ /// The [baseUri] is used as the base for resolving relative
+ /// URI references in the configuration file. If the configuration
+ /// has been read from a file, the [baseUri] can be the URI of that
+ /// file, or of the directory it occurs in.
+ ///
+ /// If [onError] is provided, errors found during parsing or building
+ /// the configuration are reported by calling [onError] instead of
+ /// throwing, and parser makes a *best effort* attempt to continue
+ /// despite the error. The input must still be valid JSON.
+ /// The result may be [PackageConfig.empty] if there is no way to
+ /// extract useful information from the bytes.
+ static PackageConfig parseString(String configuration, Uri baseUri,
+ {void Function(Object error)? onError}) =>
+ parsePackageConfigString(configuration, baseUri, onError ?? throwError);
+
+ /// Parses the JSON data of a package configuration file.
+ ///
+ /// The [jsonData] must be a JSON-like Dart data structure,
+ /// like the one provided by parsing JSON text using `dart:convert`,
+ /// containing a valid package configuration.
+ ///
+ /// The [baseUri] is used as the base for resolving relative
+ /// URI references in the configuration file. If the configuration
+ /// has been read from a file, the [baseUri] can be the URI of that
+ /// file, or of the directory it occurs in.
+ ///
+ /// If [onError] is provided, errors found during parsing or building
+ /// the configuration are reported by calling [onError] instead of
+ /// throwing, and parser makes a *best effort* attempt to continue
+ /// despite the error. The input must still be valid JSON.
+ /// The result may be [PackageConfig.empty] if there is no way to
+ /// extract useful information from the bytes.
+ static PackageConfig parseJson(Object? jsonData, Uri baseUri,
+ {void Function(Object error)? onError}) =>
+ parsePackageConfigJson(jsonData, baseUri, onError ?? throwError);
+
+ /// Writes a configuration file for this configuration on [output].
+ ///
+ /// If [baseUri] is provided, URI references in the generated file
+ /// will be made relative to [baseUri] where possible.
+ static void writeBytes(PackageConfig configuration, Sink<Uint8List> output,
+ [Uri? baseUri]) {
+ writePackageConfigJsonUtf8(configuration, baseUri, output);
+ }
+
+ /// Writes a configuration JSON text for this configuration on [output].
+ ///
+ /// If [baseUri] is provided, URI references in the generated file
+ /// will be made relative to [baseUri] where possible.
+ static void writeString(PackageConfig configuration, StringSink output,
+ [Uri? baseUri]) {
+ writePackageConfigJsonString(configuration, baseUri, output);
+ }
+
+ /// Converts a configuration to a JSON-like data structure.
+ ///
+ /// If [baseUri] is provided, URI references in the generated data
+ /// will be made relative to [baseUri] where possible.
+ static Map<String, Object?> toJson(PackageConfig configuration,
+ [Uri? baseUri]) =>
+ packageConfigToJson(configuration, baseUri);
+
+ /// The configuration version number.
+ ///
+ /// Currently this is 1 or 2, where
+ /// * Version one is the `.packages` file format and
+ /// * Version two is the first `package_config.json` format.
+ ///
+ /// Instances of this class supports both, and the version
+ /// is only useful for detecting which kind of file the configuration
+ /// was read from.
+ int get version;
+
+ /// All the available packages of this configuration.
+ ///
+ /// No two of these packages have the same name,
+ /// and no two [Package.root] directories overlap.
+ Iterable<Package> get packages;
+
+ /// Look up a package by name.
+ ///
+ /// Returns the [Package] from [packages] with [packageName] as
+ /// [Package.name]. Returns `null` if the package is not available in the
+ /// current configuration.
+ Package? operator [](String packageName);
+
+ /// Provides the associated package for a specific [file] (or directory).
+ ///
+ /// Returns a [Package] which contains the [file]'s path, if any.
+ /// That is, the [Package.root] directory is a parent directory
+ /// of the [file]'s location.
+ ///
+ /// Returns `null` if the file does not belong to any package.
+ Package? packageOf(Uri file);
+
+ /// Resolves a `package:` URI to a non-package URI
+ ///
+ /// The [packageUri] must be a valid package URI. That means:
+ /// * A URI with `package` as scheme,
+ /// * with no authority part (`package://...`),
+ /// * with a path starting with a valid package name followed by a slash, and
+ /// * with no query or fragment part.
+ ///
+ /// Throws an [ArgumentError] (which also implements [PackageConfigError])
+ /// if the package URI is not valid.
+ ///
+ /// Returns `null` if the package name of [packageUri] is not available
+ /// in this package configuration.
+ /// Returns the remaining path of the package URI resolved relative to the
+ /// [Package.packageUriRoot] of the corresponding package.
+ Uri? resolve(Uri packageUri);
+
+ /// The package URI which resolves to [nonPackageUri].
+ ///
+ /// The [nonPackageUri] must not have any query or fragment part,
+ /// and it must not have `package` as scheme.
+ /// Throws an [ArgumentError] (which also implements [PackageConfigError])
+ /// if the non-package URI is not valid.
+ ///
+ /// Returns a package URI which [resolve] will convert to [nonPackageUri],
+ /// if any such URI exists. Returns `null` if no such package URI exists.
+ Uri? toPackageUri(Uri nonPackageUri);
+
+ /// Extra data associated with the package configuration.
+ ///
+ /// The data may be in any format, depending on who introduced it.
+ /// The standard `package_config.json` file storage will only store
+ /// JSON-like list/map data structures.
+ Object? get extraData;
+}
+
+/// Configuration data for a single package.
+abstract class Package {
+ /// Creates a package with the provided properties.
+ ///
+ /// The [name] must be a valid package name.
+ /// The [root] must be an absolute directory URI, meaning an absolute URI
+ /// with no query or fragment path and a path starting and ending with `/`.
+ /// The [packageUriRoot], if provided, must be either an absolute
+ /// directory URI or a relative URI reference which is then resolved
+ /// relative to [root]. It must then also be a subdirectory of [root],
+ /// or the same directory, and must end with `/`.
+ /// If [languageVersion] is supplied, it must be a valid Dart language
+ /// version, which means two decimal integer literals separated by a `.`,
+ /// where the integer literals have no leading zeros unless they are
+ /// a single zero digit.
+ ///
+ /// The [relativeRoot] controls whether the [root] is written as
+ /// relative to the `package_config.json` file when the package
+ /// configuration is written to a file. It defaults to being relative.
+ ///
+ /// If [extraData] is supplied, it will be available as the
+ /// [Package.extraData] of the created package.
+ factory Package(String name, Uri root,
+ {Uri? packageUriRoot,
+ LanguageVersion? languageVersion,
+ Object? extraData,
+ bool relativeRoot = true}) =>
+ SimplePackage.validate(name, root, packageUriRoot, languageVersion,
+ extraData, relativeRoot, throwError)!;
+
+ /// The package-name of the package.
+ String get name;
+
+ /// The location of the root of the package.
+ ///
+ /// Is always an absolute URI with no query or fragment parts,
+ /// and with a path ending in `/`.
+ ///
+ /// All files in the [root] directory are considered
+ /// part of the package for purposes where that that matters.
+ Uri get root;
+
+ /// The root of the files available through `package:` URIs.
+ ///
+ /// A `package:` URI with [name] as the package name is
+ /// resolved relative to this location.
+ ///
+ /// Is always an absolute URI with no query or fragment part
+ /// with a path ending in `/`,
+ /// and with a location which is a subdirectory
+ /// of the [root], or the same as the [root].
+ Uri get packageUriRoot;
+
+ /// The default language version associated with this package.
+ ///
+ /// Each package may have a default language version associated,
+ /// which is the language version used to parse and compile
+ /// Dart files in the package.
+ /// A package version is defined by two non-negative numbers,
+ /// the *major* and *minor* version numbers.
+ ///
+ /// A package may have no language version associated with it
+ /// in the package configuration, in which case tools should
+ /// use a default behavior for the package.
+ LanguageVersion? get languageVersion;
+
+ /// Extra data associated with the specific package.
+ ///
+ /// The data may be in any format, depending on who introduced it.
+ /// The standard `package_config.json` file storage will only store
+ /// JSON-like list/map data structures.
+ Object? get extraData;
+
+ /// Whether the [root] URI should be written as relative.
+ ///
+ /// When the configuration is written to a `package_config.json`
+ /// file, the [root] URI can be either relative to the file
+ /// location or absolute, controller by this value.
+ bool get relativeRoot;
+}
+
+/// A language version.
+///
+/// A language version is represented by two non-negative integers,
+/// the [major] and [minor] version numbers.
+///
+/// If errors during parsing are handled using an `onError` handler,
+/// then an *invalid* language version may be represented by an
+/// [InvalidLanguageVersion] object.
+abstract class LanguageVersion implements Comparable<LanguageVersion> {
+ /// The maximal value allowed by [major] and [minor] values;
+ static const int maxValue = 0x7FFFFFFF;
+ factory LanguageVersion(int major, int minor) {
+ RangeError.checkValueInInterval(major, 0, maxValue, 'major');
+ RangeError.checkValueInInterval(minor, 0, maxValue, 'major');
+ return SimpleLanguageVersion(major, minor, null);
+ }
+
+ /// Parses a language version string.
+ ///
+ /// A valid language version string has the form
+ ///
+ /// > *decimalNumber* `.` *decimalNumber*
+ ///
+ /// where a *decimalNumber* is a non-empty sequence of decimal digits
+ /// with no unnecessary leading zeros (the decimal number only starts
+ /// with a zero digit if that digit is the entire number).
+ /// No spaces are allowed in the string.
+ ///
+ /// If the [source] is valid then it is parsed into a valid
+ /// [LanguageVersion] object.
+ /// If not, then the [onError] is called with a [FormatException].
+ /// If [onError] is not supplied, it defaults to throwing the exception.
+ /// If the call does not throw, then an [InvalidLanguageVersion] is returned
+ /// containing the original [source].
+ static LanguageVersion parse(String source,
+ {void Function(Object error)? onError}) =>
+ parseLanguageVersion(source, onError ?? throwError);
+
+ /// The major language version.
+ ///
+ /// A non-negative integer less than 2<sup>31</sup>.
+ ///
+ /// The value is negative for objects representing *invalid* language
+ /// versions ([InvalidLanguageVersion]).
+ int get major;
+
+ /// The minor language version.
+ ///
+ /// A non-negative integer less than 2<sup>31</sup>.
+ ///
+ /// The value is negative for objects representing *invalid* language
+ /// versions ([InvalidLanguageVersion]).
+ int get minor;
+
+ /// Compares language versions.
+ ///
+ /// Two language versions are considered equal if they have the
+ /// same major and minor version numbers.
+ ///
+ /// A language version is greater then another if the former's major version
+ /// is greater than the latter's major version, or if they have
+ /// the same major version and the former's minor version is greater than
+ /// the latter's.
+ @override
+ int compareTo(LanguageVersion other);
+
+ /// Valid language versions with the same [major] and [minor] values are
+ /// equal.
+ ///
+ /// Invalid language versions ([InvalidLanguageVersion]) are not equal to
+ /// any other object.
+ @override
+ bool operator ==(Object other);
+
+ @override
+ int get hashCode;
+
+ /// A string representation of the language version.
+ ///
+ /// A valid language version is represented as
+ /// `"${version.major}.${version.minor}"`.
+ @override
+ String toString();
+}
+
+/// An *invalid* language version.
+///
+/// Stored in a [Package] when the original language version string
+/// was invalid and a `onError` handler was passed to the parser
+/// which did not throw on an error.
+abstract class InvalidLanguageVersion implements LanguageVersion {
+ /// The value -1 for an invalid language version.
+ @override
+ int get major;
+
+ /// The value -1 for an invalid language version.
+ @override
+ int get minor;
+
+ /// An invalid language version is only equal to itself.
+ @override
+ bool operator ==(Object other);
+
+ @override
+ int get hashCode;
+
+ /// The original invalid version string.
+ @override
+ String toString();
+}
diff --git a/pkgs/package_config/lib/src/package_config_impl.dart b/pkgs/package_config/lib/src/package_config_impl.dart
new file mode 100644
index 0000000..865e99a
--- /dev/null
+++ b/pkgs/package_config/lib/src/package_config_impl.dart
@@ -0,0 +1,568 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'errors.dart';
+import 'package_config.dart';
+import 'util.dart';
+
+export 'package_config.dart';
+
+const bool _disallowPackagesInsidePackageUriRoot = false;
+
+// Implementations of the main data types exposed by the API of this package.
+
+class SimplePackageConfig implements PackageConfig {
+ @override
+ final int version;
+ final Map<String, Package> _packages;
+ final PackageTree _packageTree;
+ @override
+ final Object? extraData;
+
+ factory SimplePackageConfig(int version, Iterable<Package> packages,
+ [Object? extraData, void Function(Object error)? onError]) {
+ onError ??= throwError;
+ var validVersion = _validateVersion(version, onError);
+ var sortedPackages = [...packages]..sort(_compareRoot);
+ var packageTree = _validatePackages(packages, sortedPackages, onError);
+ return SimplePackageConfig._(validVersion, packageTree,
+ {for (var p in packageTree.allPackages) p.name: p}, extraData);
+ }
+
+ SimplePackageConfig._(
+ this.version, this._packageTree, this._packages, this.extraData);
+
+ /// Creates empty configuration.
+ ///
+ /// The empty configuration can be used in cases where no configuration is
+ /// found, but code expects a non-null configuration.
+ ///
+ /// The version number is [PackageConfig.maxVersion] to avoid
+ /// minimum-version filters discarding the configuration.
+ const SimplePackageConfig.empty()
+ : version = PackageConfig.maxVersion,
+ _packageTree = const EmptyPackageTree(),
+ _packages = const <String, Package>{},
+ extraData = null;
+
+ static int _validateVersion(
+ int version, void Function(Object error) onError) {
+ if (version < 0 || version > PackageConfig.maxVersion) {
+ onError(PackageConfigArgumentError(version, 'version',
+ 'Must be in the range 1 to ${PackageConfig.maxVersion}'));
+ return 2; // The minimal version supporting a SimplePackageConfig.
+ }
+ return version;
+ }
+
+ static PackageTree _validatePackages(Iterable<Package> originalPackages,
+ List<Package> packages, void Function(Object error) onError) {
+ var packageNames = <String>{};
+ var tree = TriePackageTree();
+ for (var originalPackage in packages) {
+ SimplePackage? newPackage;
+ if (originalPackage is! SimplePackage) {
+ // SimplePackage validates these properties.
+ newPackage = SimplePackage.validate(
+ originalPackage.name,
+ originalPackage.root,
+ originalPackage.packageUriRoot,
+ originalPackage.languageVersion,
+ originalPackage.extraData,
+ originalPackage.relativeRoot, (error) {
+ if (error is PackageConfigArgumentError) {
+ onError(PackageConfigArgumentError(packages, 'packages',
+ 'Package ${newPackage!.name}: ${error.message}'));
+ } else {
+ onError(error);
+ }
+ });
+ if (newPackage == null) continue;
+ } else {
+ newPackage = originalPackage;
+ }
+ var name = newPackage.name;
+ if (packageNames.contains(name)) {
+ onError(PackageConfigArgumentError(
+ name, 'packages', "Duplicate package name '$name'"));
+ continue;
+ }
+ packageNames.add(name);
+ tree.add(newPackage, (error) {
+ if (error is ConflictException) {
+ // There is a conflict with an existing package.
+ var existingPackage = error.existingPackage;
+ switch (error.conflictType) {
+ case ConflictType.sameRoots:
+ onError(PackageConfigArgumentError(
+ originalPackages,
+ 'packages',
+ 'Packages ${newPackage!.name} and ${existingPackage.name} '
+ 'have the same root directory: ${newPackage.root}.\n'));
+ break;
+ case ConflictType.interleaving:
+ // The new package is inside the package URI root of the existing
+ // package.
+ onError(PackageConfigArgumentError(
+ originalPackages,
+ 'packages',
+ 'Package ${newPackage!.name} is inside the root of '
+ 'package ${existingPackage.name}, and the package root '
+ 'of ${existingPackage.name} is inside the root of '
+ '${newPackage.name}.\n'
+ '${existingPackage.name} package root: '
+ '${existingPackage.packageUriRoot}\n'
+ '${newPackage.name} root: ${newPackage.root}\n'));
+ break;
+ case ConflictType.insidePackageRoot:
+ onError(PackageConfigArgumentError(
+ originalPackages,
+ 'packages',
+ 'Package ${newPackage!.name} is inside the package root of '
+ 'package ${existingPackage.name}.\n'
+ '${existingPackage.name} package root: '
+ '${existingPackage.packageUriRoot}\n'
+ '${newPackage.name} root: ${newPackage.root}\n'));
+ break;
+ }
+ } else {
+ // Any other error.
+ onError(error);
+ }
+ });
+ }
+ return tree;
+ }
+
+ @override
+ Iterable<Package> get packages => _packages.values;
+
+ @override
+ Package? operator [](String packageName) => _packages[packageName];
+
+ @override
+ Package? packageOf(Uri file) => _packageTree.packageOf(file);
+
+ @override
+ Uri? resolve(Uri packageUri) {
+ var packageName = checkValidPackageUri(packageUri, 'packageUri');
+ return _packages[packageName]?.packageUriRoot.resolveUri(
+ Uri(path: packageUri.path.substring(packageName.length + 1)));
+ }
+
+ @override
+ Uri? toPackageUri(Uri nonPackageUri) {
+ if (nonPackageUri.isScheme('package')) {
+ throw PackageConfigArgumentError(
+ nonPackageUri, 'nonPackageUri', 'Must not be a package URI');
+ }
+ if (nonPackageUri.hasQuery || nonPackageUri.hasFragment) {
+ throw PackageConfigArgumentError(nonPackageUri, 'nonPackageUri',
+ 'Must not have query or fragment part');
+ }
+ // Find package that file belongs to.
+ var package = _packageTree.packageOf(nonPackageUri);
+ if (package == null) return null;
+ // Check if it is inside the package URI root.
+ var path = nonPackageUri.toString();
+ var root = package.packageUriRoot.toString();
+ if (_beginsWith(package.root.toString().length, root, path)) {
+ var rest = path.substring(root.length);
+ return Uri(scheme: 'package', path: '${package.name}/$rest');
+ }
+ return null;
+ }
+}
+
+/// Configuration data for a single package.
+class SimplePackage implements Package {
+ @override
+ final String name;
+ @override
+ final Uri root;
+ @override
+ final Uri packageUriRoot;
+ @override
+ final LanguageVersion? languageVersion;
+ @override
+ final Object? extraData;
+ @override
+ final bool relativeRoot;
+
+ SimplePackage._(this.name, this.root, this.packageUriRoot,
+ this.languageVersion, this.extraData, this.relativeRoot);
+
+ /// Creates a [SimplePackage] with the provided content.
+ ///
+ /// The provided arguments must be valid.
+ ///
+ /// If the arguments are invalid then the error is reported by
+ /// calling [onError], then the erroneous entry is ignored.
+ ///
+ /// If [onError] is provided, the user is expected to be able to handle
+ /// errors themselves. An invalid [languageVersion] string
+ /// will be replaced with the string `"invalid"`. This allows
+ /// users to detect the difference between an absent version and
+ /// an invalid one.
+ ///
+ /// Returns `null` if the input is invalid and an approximately valid package
+ /// cannot be salvaged from the input.
+ static SimplePackage? validate(
+ String name,
+ Uri root,
+ Uri? packageUriRoot,
+ LanguageVersion? languageVersion,
+ Object? extraData,
+ bool relativeRoot,
+ void Function(Object error) onError) {
+ var fatalError = false;
+ var invalidIndex = checkPackageName(name);
+ if (invalidIndex >= 0) {
+ onError(PackageConfigFormatException(
+ 'Not a valid package name', name, invalidIndex));
+ fatalError = true;
+ }
+ if (root.isScheme('package')) {
+ onError(PackageConfigArgumentError(
+ '$root', 'root', 'Must not be a package URI'));
+ fatalError = true;
+ } else if (!isAbsoluteDirectoryUri(root)) {
+ onError(PackageConfigArgumentError(
+ '$root',
+ 'root',
+ 'In package $name: Not an absolute URI with no query or fragment '
+ 'with a path ending in /'));
+ // Try to recover. If the URI has a scheme,
+ // then ensure that the path ends with `/`.
+ if (!root.hasScheme) {
+ fatalError = true;
+ } else if (!root.path.endsWith('/')) {
+ root = root.replace(path: '${root.path}/');
+ }
+ }
+ if (packageUriRoot == null) {
+ packageUriRoot = root;
+ } else if (!fatalError) {
+ packageUriRoot = root.resolveUri(packageUriRoot);
+ if (!isAbsoluteDirectoryUri(packageUriRoot)) {
+ onError(PackageConfigArgumentError(
+ packageUriRoot,
+ 'packageUriRoot',
+ 'In package $name: Not an absolute URI with no query or fragment '
+ 'with a path ending in /'));
+ packageUriRoot = root;
+ } else if (!isUriPrefix(root, packageUriRoot)) {
+ onError(PackageConfigArgumentError(packageUriRoot, 'packageUriRoot',
+ 'The package URI root is not below the package root'));
+ packageUriRoot = root;
+ }
+ }
+ if (fatalError) return null;
+ return SimplePackage._(
+ name, root, packageUriRoot, languageVersion, extraData, relativeRoot);
+ }
+}
+
+/// Checks whether [source] is a valid Dart language version string.
+///
+/// The format is (as RegExp) `^(0|[1-9]\d+)\.(0|[1-9]\d+)$`.
+///
+/// Reports a format exception on [onError] if not, or if the numbers
+/// are too large (at most 32-bit signed integers).
+LanguageVersion parseLanguageVersion(
+ String? source, void Function(Object error) onError) {
+ var index = 0;
+ // Reads a positive decimal numeral. Returns the value of the numeral,
+ // or a negative number in case of an error.
+ // Starts at [index] and increments the index to the position after
+ // the numeral.
+ // It is an error if the numeral value is greater than 0x7FFFFFFFF.
+ // It is a recoverable error if the numeral starts with leading zeros.
+ int readNumeral() {
+ const maxValue = 0x7FFFFFFF;
+ if (index == source!.length) {
+ onError(PackageConfigFormatException('Missing number', source, index));
+ return -1;
+ }
+ var start = index;
+
+ var char = source.codeUnitAt(index);
+ var digit = char ^ 0x30;
+ if (digit > 9) {
+ onError(PackageConfigFormatException('Missing number', source, index));
+ return -1;
+ }
+ var firstDigit = digit;
+ var value = 0;
+ do {
+ value = value * 10 + digit;
+ if (value > maxValue) {
+ onError(
+ PackageConfigFormatException('Number too large', source, start));
+ return -1;
+ }
+ index++;
+ if (index == source.length) break;
+ char = source.codeUnitAt(index);
+ digit = char ^ 0x30;
+ } while (digit <= 9);
+ if (firstDigit == 0 && index > start + 1) {
+ onError(PackageConfigFormatException(
+ 'Leading zero not allowed', source, start));
+ }
+ return value;
+ }
+
+ var major = readNumeral();
+ if (major < 0) {
+ return SimpleInvalidLanguageVersion(source);
+ }
+ if (index == source!.length || source.codeUnitAt(index) != $dot) {
+ onError(PackageConfigFormatException("Missing '.'", source, index));
+ return SimpleInvalidLanguageVersion(source);
+ }
+ index++;
+ var minor = readNumeral();
+ if (minor < 0) {
+ return SimpleInvalidLanguageVersion(source);
+ }
+ if (index != source.length) {
+ onError(PackageConfigFormatException(
+ 'Unexpected trailing character', source, index));
+ return SimpleInvalidLanguageVersion(source);
+ }
+ return SimpleLanguageVersion(major, minor, source);
+}
+
+abstract class _SimpleLanguageVersionBase implements LanguageVersion {
+ @override
+ int compareTo(LanguageVersion other) {
+ var result = major.compareTo(other.major);
+ if (result != 0) return result;
+ return minor.compareTo(other.minor);
+ }
+}
+
+class SimpleLanguageVersion extends _SimpleLanguageVersionBase {
+ @override
+ final int major;
+ @override
+ final int minor;
+ String? _source;
+ SimpleLanguageVersion(this.major, this.minor, this._source);
+
+ @override
+ bool operator ==(Object other) =>
+ other is LanguageVersion && major == other.major && minor == other.minor;
+
+ @override
+ int get hashCode => (major * 17 ^ minor * 37) & 0x3FFFFFFF;
+
+ @override
+ String toString() => _source ??= '$major.$minor';
+}
+
+class SimpleInvalidLanguageVersion extends _SimpleLanguageVersionBase
+ implements InvalidLanguageVersion {
+ final String? _source;
+ SimpleInvalidLanguageVersion(this._source);
+ @override
+ int get major => -1;
+ @override
+ int get minor => -1;
+
+ @override
+ String toString() => _source!;
+}
+
+abstract class PackageTree {
+ Iterable<Package> get allPackages;
+ SimplePackage? packageOf(Uri file);
+}
+
+class _PackageTrieNode {
+ SimplePackage? package;
+
+ /// Indexed by path segment.
+ Map<String, _PackageTrieNode> map = {};
+}
+
+/// Packages of a package configuration ordered by root path.
+///
+/// A package has a root path and a package root path, where the latter
+/// contains the files exposed by `package:` URIs.
+///
+/// A package is said to be inside another package if the root path URI of
+/// the latter is a prefix of the root path URI of the former.
+///
+/// No two packages of a package may have the same root path.
+/// The package root path of a package must not be inside another package's
+/// root path.
+/// Entire other packages are allowed inside a package's root.
+class TriePackageTree implements PackageTree {
+ /// Indexed by URI scheme.
+ final Map<String, _PackageTrieNode> _map = {};
+
+ /// A list of all packages.
+ final List<SimplePackage> _packages = [];
+
+ @override
+ Iterable<Package> get allPackages sync* {
+ for (var package in _packages) {
+ yield package;
+ }
+ }
+
+ bool _checkConflict(_PackageTrieNode node, SimplePackage newPackage,
+ void Function(Object error) onError) {
+ var existingPackage = node.package;
+ if (existingPackage != null) {
+ // Trying to add package that is inside the existing package.
+ // 1) If it's an exact match it's not allowed (i.e. the roots can't be
+ // the same).
+ if (newPackage.root.path.length == existingPackage.root.path.length) {
+ onError(ConflictException(
+ newPackage, existingPackage, ConflictType.sameRoots));
+ return true;
+ }
+ // 2) The existing package has a packageUriRoot thats inside the
+ // root of the new package.
+ if (_beginsWith(0, newPackage.root.toString(),
+ existingPackage.packageUriRoot.toString())) {
+ onError(ConflictException(
+ newPackage, existingPackage, ConflictType.interleaving));
+ return true;
+ }
+
+ // For internal reasons we allow this (for now). One should still never do
+ // it though.
+ // 3) The new package is inside the packageUriRoot of existing package.
+ if (_disallowPackagesInsidePackageUriRoot) {
+ if (_beginsWith(0, existingPackage.packageUriRoot.toString(),
+ newPackage.root.toString())) {
+ onError(ConflictException(
+ newPackage, existingPackage, ConflictType.insidePackageRoot));
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /// Tries to add `newPackage` to the tree.
+ ///
+ /// Reports a [ConflictException] if the added package conflicts with an
+ /// existing package.
+ /// It conflicts if its root or package root is the same as an existing
+ /// package's root or package root, is between the two, or if it's inside the
+ /// package root of an existing package.
+ ///
+ /// If a conflict is detected between [newPackage] and a previous package,
+ /// then [onError] is called with a [ConflictException] object
+ /// and the [newPackage] is not added to the tree.
+ ///
+ /// The packages are added in order of their root path.
+ void add(SimplePackage newPackage, void Function(Object error) onError) {
+ var root = newPackage.root;
+ var node = _map[root.scheme] ??= _PackageTrieNode();
+ if (_checkConflict(node, newPackage, onError)) return;
+ var segments = root.pathSegments;
+ // Notice that we're skipping the last segment as it's always the empty
+ // string because roots are directories.
+ for (var i = 0; i < segments.length - 1; i++) {
+ var path = segments[i];
+ node = node.map[path] ??= _PackageTrieNode();
+ if (_checkConflict(node, newPackage, onError)) return;
+ }
+ node.package = newPackage;
+ _packages.add(newPackage);
+ }
+
+ bool _isMatch(
+ String path, _PackageTrieNode node, List<SimplePackage> potential) {
+ var currentPackage = node.package;
+ if (currentPackage != null) {
+ var currentPackageRootLength = currentPackage.root.toString().length;
+ if (path.length == currentPackageRootLength) return true;
+ var currentPackageUriRoot = currentPackage.packageUriRoot.toString();
+ // Is [file] inside the package root of [currentPackage]?
+ if (currentPackageUriRoot.length == currentPackageRootLength ||
+ _beginsWith(currentPackageRootLength, currentPackageUriRoot, path)) {
+ return true;
+ }
+ potential.add(currentPackage);
+ }
+ return false;
+ }
+
+ @override
+ SimplePackage? packageOf(Uri file) {
+ var currentTrieNode = _map[file.scheme];
+ if (currentTrieNode == null) return null;
+ var path = file.toString();
+ var potential = <SimplePackage>[];
+ if (_isMatch(path, currentTrieNode, potential)) {
+ return currentTrieNode.package;
+ }
+ var segments = file.pathSegments;
+
+ for (var i = 0; i < segments.length - 1; i++) {
+ var segment = segments[i];
+ currentTrieNode = currentTrieNode!.map[segment];
+ if (currentTrieNode == null) break;
+ if (_isMatch(path, currentTrieNode, potential)) {
+ return currentTrieNode.package;
+ }
+ }
+ if (potential.isEmpty) return null;
+ return potential.last;
+ }
+}
+
+class EmptyPackageTree implements PackageTree {
+ const EmptyPackageTree();
+
+ @override
+ Iterable<Package> get allPackages => const Iterable<Package>.empty();
+
+ @override
+ SimplePackage? packageOf(Uri file) => null;
+}
+
+/// Checks whether [longerPath] begins with [parentPath].
+///
+/// Skips checking the [start] first characters which are assumed to
+/// already have been matched.
+bool _beginsWith(int start, String parentPath, String longerPath) {
+ if (longerPath.length < parentPath.length) return false;
+ for (var i = start; i < parentPath.length; i++) {
+ if (longerPath.codeUnitAt(i) != parentPath.codeUnitAt(i)) return false;
+ }
+ return true;
+}
+
+enum ConflictType { sameRoots, interleaving, insidePackageRoot }
+
+/// Conflict between packages added to the same configuration.
+///
+/// The [package] conflicts with [existingPackage] if it has
+/// the same root path or the package URI root path
+/// of [existingPackage] is inside the root path of [package].
+class ConflictException {
+ /// The existing package that [package] conflicts with.
+ final SimplePackage existingPackage;
+
+ /// The package that could not be added without a conflict.
+ final SimplePackage package;
+
+ /// Whether the conflict is with the package URI root of [existingPackage].
+ final ConflictType conflictType;
+
+ /// Creates a root conflict between [package] and [existingPackage].
+ ConflictException(this.package, this.existingPackage, this.conflictType);
+}
+
+/// Used for sorting packages by root path.
+int _compareRoot(Package p1, Package p2) =>
+ p1.root.toString().compareTo(p2.root.toString());
diff --git a/pkgs/package_config/lib/src/package_config_io.dart b/pkgs/package_config/lib/src/package_config_io.dart
new file mode 100644
index 0000000..8c5773b
--- /dev/null
+++ b/pkgs/package_config/lib/src/package_config_io.dart
@@ -0,0 +1,166 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// dart:io dependent functionality for reading and writing configuration files.
+
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'errors.dart';
+import 'package_config_impl.dart';
+import 'package_config_json.dart';
+import 'packages_file.dart' as packages_file;
+import 'util.dart';
+import 'util_io.dart';
+
+/// Name of directory where Dart tools store their configuration.
+///
+/// Directory is created in the package root directory.
+const dartToolDirName = '.dart_tool';
+
+/// Name of file containing new package configuration data.
+///
+/// File is stored in the dart tool directory.
+const packageConfigFileName = 'package_config.json';
+
+/// Name of file containing legacy package configuration data.
+///
+/// File is stored in the package root directory.
+const packagesFileName = '.packages';
+
+/// Reads a package configuration file.
+///
+/// Detects whether the [file] is a version one `.packages` file or
+/// a version two `package_config.json` file.
+///
+/// If the [file] is a `.packages` file and [preferNewest] is true,
+/// first checks whether there is an adjacent `.dart_tool/package_config.json`
+/// file, and if so, reads that instead.
+/// If [preferNewest] is false, the specified file is loaded even if it is
+/// a `.packages` file and there is an available `package_config.json` file.
+///
+/// The file must exist and be a normal file.
+Future<PackageConfig> readAnyConfigFile(
+ File file, bool preferNewest, void Function(Object error) onError) async {
+ if (preferNewest && fileName(file.path) == packagesFileName) {
+ var alternateFile = File(
+ pathJoin(dirName(file.path), dartToolDirName, packageConfigFileName));
+ if (alternateFile.existsSync()) {
+ return await readPackageConfigJsonFile(alternateFile, onError);
+ }
+ }
+ Uint8List bytes;
+ try {
+ bytes = await file.readAsBytes();
+ } catch (e) {
+ onError(e);
+ return const SimplePackageConfig.empty();
+ }
+ return parseAnyConfigFile(bytes, file.uri, onError);
+}
+
+/// Like [readAnyConfigFile] but uses a URI and an optional loader.
+Future<PackageConfig> readAnyConfigFileUri(
+ Uri file,
+ Future<Uint8List?> Function(Uri uri)? loader,
+ void Function(Object error) onError,
+ bool preferNewest) async {
+ if (file.isScheme('package')) {
+ throw PackageConfigArgumentError(
+ file, 'file', 'Must not be a package: URI');
+ }
+ if (loader == null) {
+ if (file.isScheme('file')) {
+ return await readAnyConfigFile(File.fromUri(file), preferNewest, onError);
+ }
+ loader = defaultLoader;
+ }
+ if (preferNewest && file.pathSegments.last == packagesFileName) {
+ var alternateFile = file.resolve('$dartToolDirName/$packageConfigFileName');
+ Uint8List? bytes;
+ try {
+ bytes = await loader(alternateFile);
+ } catch (e) {
+ onError(e);
+ return const SimplePackageConfig.empty();
+ }
+ if (bytes != null) {
+ return parsePackageConfigBytes(bytes, alternateFile, onError);
+ }
+ }
+ Uint8List? bytes;
+ try {
+ bytes = await loader(file);
+ } catch (e) {
+ onError(e);
+ return const SimplePackageConfig.empty();
+ }
+ if (bytes == null) {
+ onError(PackageConfigArgumentError(
+ file.toString(), 'file', 'File cannot be read'));
+ return const SimplePackageConfig.empty();
+ }
+ return parseAnyConfigFile(bytes, file, onError);
+}
+
+/// Parses a `.packages` or `package_config.json` file's contents.
+///
+/// Assumes it's a JSON file if the first non-whitespace character
+/// is `{`, otherwise assumes it's a `.packages` file.
+PackageConfig parseAnyConfigFile(
+ Uint8List bytes, Uri file, void Function(Object error) onError) {
+ var firstChar = firstNonWhitespaceChar(bytes);
+ if (firstChar != $lbrace) {
+ // Definitely not a JSON object, probably a .packages.
+ return packages_file.parse(bytes, file, onError);
+ }
+ return parsePackageConfigBytes(bytes, file, onError);
+}
+
+Future<PackageConfig> readPackageConfigJsonFile(
+ File file, void Function(Object error) onError) async {
+ Uint8List bytes;
+ try {
+ bytes = await file.readAsBytes();
+ } catch (error) {
+ onError(error);
+ return const SimplePackageConfig.empty();
+ }
+ return parsePackageConfigBytes(bytes, file.uri, onError);
+}
+
+Future<PackageConfig> readDotPackagesFile(
+ File file, void Function(Object error) onError) async {
+ Uint8List bytes;
+ try {
+ bytes = await file.readAsBytes();
+ } catch (error) {
+ onError(error);
+ return const SimplePackageConfig.empty();
+ }
+ return packages_file.parse(bytes, file.uri, onError);
+}
+
+Future<void> writePackageConfigJsonFile(
+ PackageConfig config, Directory targetDirectory) async {
+ // Write .dart_tool/package_config.json first.
+ var dartToolDir = Directory(pathJoin(targetDirectory.path, dartToolDirName));
+ await dartToolDir.create(recursive: true);
+ var file = File(pathJoin(dartToolDir.path, packageConfigFileName));
+ var baseUri = file.uri;
+
+ var sink = file.openWrite(encoding: utf8);
+ writePackageConfigJsonUtf8(config, baseUri, sink);
+ var doneJson = sink.close();
+
+ // Write .packages too.
+ file = File(pathJoin(targetDirectory.path, packagesFileName));
+ baseUri = file.uri;
+ sink = file.openWrite(encoding: utf8);
+ writeDotPackages(config, baseUri, sink);
+ var donePackages = sink.close();
+
+ await Future.wait([doneJson, donePackages]);
+}
diff --git a/pkgs/package_config/lib/src/package_config_json.dart b/pkgs/package_config/lib/src/package_config_json.dart
new file mode 100644
index 0000000..65560a0
--- /dev/null
+++ b/pkgs/package_config/lib/src/package_config_json.dart
@@ -0,0 +1,321 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Parsing and serialization of package configurations.
+
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'errors.dart';
+import 'package_config_impl.dart';
+import 'packages_file.dart' as packages_file;
+import 'util.dart';
+
+const String _configVersionKey = 'configVersion';
+const String _packagesKey = 'packages';
+const List<String> _topNames = [_configVersionKey, _packagesKey];
+const String _nameKey = 'name';
+const String _rootUriKey = 'rootUri';
+const String _packageUriKey = 'packageUri';
+const String _languageVersionKey = 'languageVersion';
+const List<String> _packageNames = [
+ _nameKey,
+ _rootUriKey,
+ _packageUriKey,
+ _languageVersionKey
+];
+
+const String _generatedKey = 'generated';
+const String _generatorKey = 'generator';
+const String _generatorVersionKey = 'generatorVersion';
+
+final _jsonUtf8Decoder = json.fuse(utf8).decoder;
+
+PackageConfig parsePackageConfigBytes(
+ Uint8List bytes, Uri file, void Function(Object error) onError) {
+ // TODO(lrn): Make this simpler. Maybe parse directly from bytes.
+ Object? jsonObject;
+ try {
+ jsonObject = _jsonUtf8Decoder.convert(bytes);
+ } on FormatException catch (e) {
+ onError(PackageConfigFormatException.from(e));
+ return const SimplePackageConfig.empty();
+ }
+ return parsePackageConfigJson(jsonObject, file, onError);
+}
+
+PackageConfig parsePackageConfigString(
+ String source, Uri file, void Function(Object error) onError) {
+ Object? jsonObject;
+ try {
+ jsonObject = jsonDecode(source);
+ } on FormatException catch (e) {
+ onError(PackageConfigFormatException.from(e));
+ return const SimplePackageConfig.empty();
+ }
+ return parsePackageConfigJson(jsonObject, file, onError);
+}
+
+/// Creates a [PackageConfig] from a parsed JSON-like object structure.
+///
+/// The [json] argument must be a JSON object (`Map<String, Object?>`)
+/// containing a `"configVersion"` entry with an integer value in the range
+/// 1 to [PackageConfig.maxVersion],
+/// and with a `"packages"` entry which is a JSON array (`List<Object?>`)
+/// containing JSON objects which each has the following properties:
+///
+/// * `"name"`: The package name as a string.
+/// * `"rootUri"`: The root of the package as a URI stored as a string.
+/// * `"packageUri"`: Optionally the root of for `package:` URI resolution
+/// for the package, as a relative URI below the root URI
+/// stored as a string.
+/// * `"languageVersion"`: Optionally a language version string which is a
+/// an integer numeral, a decimal point (`.`) and another integer numeral,
+/// where the integer numeral cannot have a sign, and can only have a
+/// leading zero if the entire numeral is a single zero.
+///
+/// The [baseLocation] is used as base URI to resolve the "rootUri"
+/// URI reference string.
+PackageConfig parsePackageConfigJson(
+ Object? json, Uri baseLocation, void Function(Object error) onError) {
+ if (!baseLocation.hasScheme || baseLocation.isScheme('package')) {
+ throw PackageConfigArgumentError(baseLocation.toString(), 'baseLocation',
+ 'Must be an absolute non-package: URI');
+ }
+
+ if (!baseLocation.path.endsWith('/')) {
+ baseLocation = baseLocation.resolveUri(Uri(path: '.'));
+ }
+
+ String typeName<T>() {
+ if (0 is T) return 'int';
+ if ('' is T) return 'string';
+ if (const <Object?>[] is T) return 'array';
+ return 'object';
+ }
+
+ T? checkType<T>(Object? value, String name, [String? packageName]) {
+ if (value is T) return value;
+ // The only types we are called with are [int], [String], [List<Object?>]
+ // and Map<String, Object?>. Recognize which to give a better error message.
+ var message =
+ "$name${packageName != null ? " of package $packageName" : ""}"
+ ' is not a JSON ${typeName<T>()}';
+ onError(PackageConfigFormatException(message, value));
+ return null;
+ }
+
+ Package? parsePackage(Map<String, Object?> entry) {
+ String? name;
+ String? rootUri;
+ String? packageUri;
+ String? languageVersion;
+ Map<String, Object?>? extraData;
+ var hasName = false;
+ var hasRoot = false;
+ var hasVersion = false;
+ entry.forEach((key, value) {
+ switch (key) {
+ case _nameKey:
+ hasName = true;
+ name = checkType<String>(value, _nameKey);
+ break;
+ case _rootUriKey:
+ hasRoot = true;
+ rootUri = checkType<String>(value, _rootUriKey, name);
+ break;
+ case _packageUriKey:
+ packageUri = checkType<String>(value, _packageUriKey, name);
+ break;
+ case _languageVersionKey:
+ hasVersion = true;
+ languageVersion = checkType<String>(value, _languageVersionKey, name);
+ break;
+ default:
+ (extraData ??= {})[key] = value;
+ break;
+ }
+ });
+ if (!hasName) {
+ onError(PackageConfigFormatException('Missing name entry', entry));
+ }
+ if (!hasRoot) {
+ onError(PackageConfigFormatException('Missing rootUri entry', entry));
+ }
+ if (name == null || rootUri == null) return null;
+ var parsedRootUri = Uri.parse(rootUri!);
+ var relativeRoot = !hasAbsolutePath(parsedRootUri);
+ var root = baseLocation.resolveUri(parsedRootUri);
+ if (!root.path.endsWith('/')) root = root.replace(path: '${root.path}/');
+ var packageRoot = root;
+ if (packageUri != null) packageRoot = root.resolve(packageUri!);
+ if (!packageRoot.path.endsWith('/')) {
+ packageRoot = packageRoot.replace(path: '${packageRoot.path}/');
+ }
+
+ LanguageVersion? version;
+ if (languageVersion != null) {
+ version = parseLanguageVersion(languageVersion, onError);
+ } else if (hasVersion) {
+ version = SimpleInvalidLanguageVersion('invalid');
+ }
+
+ return SimplePackage.validate(
+ name!, root, packageRoot, version, extraData, relativeRoot, (error) {
+ if (error is ArgumentError) {
+ onError(
+ PackageConfigFormatException(
+ error.message.toString(), error.invalidValue),
+ );
+ } else {
+ onError(error);
+ }
+ });
+ }
+
+ var map = checkType<Map<String, Object?>>(json, 'value');
+ if (map == null) return const SimplePackageConfig.empty();
+ Map<String, Object?>? extraData;
+ List<Package>? packageList;
+ int? configVersion;
+ map.forEach((key, value) {
+ switch (key) {
+ case _configVersionKey:
+ configVersion = checkType<int>(value, _configVersionKey) ?? 2;
+ break;
+ case _packagesKey:
+ var packageArray = checkType<List<Object?>>(value, _packagesKey) ?? [];
+ var packages = <Package>[];
+ for (var package in packageArray) {
+ var packageMap =
+ checkType<Map<String, Object?>>(package, 'package entry');
+ if (packageMap != null) {
+ var entry = parsePackage(packageMap);
+ if (entry != null) {
+ packages.add(entry);
+ }
+ }
+ }
+ packageList = packages;
+ break;
+ default:
+ (extraData ??= {})[key] = value;
+ break;
+ }
+ });
+ if (configVersion == null) {
+ onError(PackageConfigFormatException('Missing configVersion entry', json));
+ configVersion = 2;
+ }
+ if (packageList == null) {
+ onError(PackageConfigFormatException('Missing packages list', json));
+ packageList = [];
+ }
+ return SimplePackageConfig(configVersion!, packageList!, extraData, (error) {
+ if (error is ArgumentError) {
+ onError(
+ PackageConfigFormatException(
+ error.message.toString(), error.invalidValue),
+ );
+ } else {
+ onError(error);
+ }
+ });
+}
+
+final _jsonUtf8Encoder = JsonUtf8Encoder(' ');
+
+void writePackageConfigJsonUtf8(
+ PackageConfig config, Uri? baseUri, Sink<List<int>> output) {
+ // Can be optimized.
+ var data = packageConfigToJson(config, baseUri);
+ output.add(_jsonUtf8Encoder.convert(data) as Uint8List);
+}
+
+void writePackageConfigJsonString(
+ PackageConfig config, Uri? baseUri, StringSink output) {
+ // Can be optimized.
+ var data = packageConfigToJson(config, baseUri);
+ output.write(const JsonEncoder.withIndent(' ').convert(data));
+}
+
+Map<String, Object?> packageConfigToJson(PackageConfig config, Uri? baseUri) =>
+ <String, Object?>{
+ ...?_extractExtraData(config.extraData, _topNames),
+ _configVersionKey: PackageConfig.maxVersion,
+ _packagesKey: [
+ for (var package in config.packages)
+ <String, Object?>{
+ _nameKey: package.name,
+ _rootUriKey: trailingSlash((package.relativeRoot
+ ? relativizeUri(package.root, baseUri)
+ : package.root)
+ .toString()),
+ if (package.root != package.packageUriRoot)
+ _packageUriKey: trailingSlash(
+ relativizeUri(package.packageUriRoot, package.root)
+ .toString()),
+ if (package.languageVersion != null &&
+ package.languageVersion is! InvalidLanguageVersion)
+ _languageVersionKey: package.languageVersion.toString(),
+ ...?_extractExtraData(package.extraData, _packageNames),
+ }
+ ],
+ };
+
+void writeDotPackages(PackageConfig config, Uri baseUri, StringSink output) {
+ var extraData = config.extraData;
+ // Write .packages too.
+ String? comment;
+ if (extraData is Map<String, Object?>) {
+ var generator = extraData[_generatorKey];
+ if (generator is String) {
+ var generated = extraData[_generatedKey];
+ var generatorVersion = extraData[_generatorVersionKey];
+ comment = 'Generated by $generator'
+ "${generatorVersion is String ? " $generatorVersion" : ""}"
+ "${generated is String ? " on $generated" : ""}.";
+ }
+ }
+ packages_file.write(output, config, baseUri: baseUri, comment: comment);
+}
+
+/// If "extraData" is a JSON map, then return it, otherwise return null.
+///
+/// If the value contains any of the [reservedNames] for the current context,
+/// entries with that name in the extra data are dropped.
+Map<String, Object?>? _extractExtraData(
+ Object? data, Iterable<String> reservedNames) {
+ if (data is Map<String, Object?>) {
+ if (data.isEmpty) return null;
+ for (var name in reservedNames) {
+ if (data.containsKey(name)) {
+ var filteredData = {
+ for (var key in data.keys)
+ if (!reservedNames.contains(key)) key: data[key]
+ };
+ if (filteredData.isEmpty) return null;
+ for (var value in filteredData.values) {
+ if (!_validateJson(value)) return null;
+ }
+ return filteredData;
+ }
+ }
+ return data;
+ }
+ return null;
+}
+
+/// Checks that the object is a valid JSON-like data structure.
+bool _validateJson(Object? object) {
+ if (object == null || true == object || false == object) return true;
+ if (object is num || object is String) return true;
+ if (object is List<Object?>) {
+ return object.every(_validateJson);
+ }
+ if (object is Map<String, Object?>) {
+ return object.values.every(_validateJson);
+ }
+ return false;
+}
diff --git a/pkgs/package_config/lib/src/packages_file.dart b/pkgs/package_config/lib/src/packages_file.dart
new file mode 100644
index 0000000..bf68f2c
--- /dev/null
+++ b/pkgs/package_config/lib/src/packages_file.dart
@@ -0,0 +1,193 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'errors.dart';
+import 'package_config_impl.dart';
+import 'util.dart';
+
+/// The language version prior to the release of language versioning.
+///
+/// This is the default language version used by all packages from a
+/// `.packages` file.
+final LanguageVersion _languageVersion = LanguageVersion(2, 7);
+
+/// Parses a `.packages` file into a [PackageConfig].
+///
+/// The [source] is the byte content of a `.packages` file, assumed to be
+/// UTF-8 encoded. In practice, all significant parts of the file must be ASCII,
+/// so Latin-1 or Windows-1252 encoding will also work fine.
+///
+/// If the file content is available as a string, its [String.codeUnits] can
+/// be used as the `source` argument of this function.
+///
+/// The [baseLocation] is used as a base URI to resolve all relative
+/// URI references against.
+/// If the content was read from a file, `baseLocation` should be the
+/// location of that file.
+///
+/// Returns a simple package configuration where each package's
+/// [Package.packageUriRoot] is the same as its [Package.root]
+/// and it has no [Package.languageVersion].
+PackageConfig parse(
+ List<int> source, Uri baseLocation, void Function(Object error) onError) {
+ if (baseLocation.isScheme('package')) {
+ onError(PackageConfigArgumentError(
+ baseLocation, 'baseLocation', 'Must not be a package: URI'));
+ return PackageConfig.empty;
+ }
+ var index = 0;
+ var packages = <Package>[];
+ var packageNames = <String>{};
+ while (index < source.length) {
+ var ignoreLine = false;
+ var start = index;
+ var separatorIndex = -1;
+ var end = source.length;
+ var char = source[index++];
+ if (char == $cr || char == $lf) {
+ continue;
+ }
+ if (char == $colon) {
+ onError(PackageConfigFormatException(
+ 'Missing package name', source, index - 1));
+ ignoreLine = true; // Ignore if package name is invalid.
+ } else {
+ ignoreLine = char == $hash; // Ignore if comment.
+ }
+ var queryStart = -1;
+ var fragmentStart = -1;
+ while (index < source.length) {
+ char = source[index++];
+ if (char == $colon && separatorIndex < 0) {
+ separatorIndex = index - 1;
+ } else if (char == $cr || char == $lf) {
+ end = index - 1;
+ break;
+ } else if (char == $question && queryStart < 0 && fragmentStart < 0) {
+ queryStart = index - 1;
+ } else if (char == $hash && fragmentStart < 0) {
+ fragmentStart = index - 1;
+ }
+ }
+ if (ignoreLine) continue;
+ if (separatorIndex < 0) {
+ onError(
+ PackageConfigFormatException("No ':' on line", source, index - 1));
+ continue;
+ }
+ var packageName = String.fromCharCodes(source, start, separatorIndex);
+ var invalidIndex = checkPackageName(packageName);
+ if (invalidIndex >= 0) {
+ onError(PackageConfigFormatException(
+ 'Not a valid package name', source, start + invalidIndex));
+ continue;
+ }
+ if (queryStart >= 0) {
+ onError(PackageConfigFormatException(
+ 'Location URI must not have query', source, queryStart));
+ end = queryStart;
+ } else if (fragmentStart >= 0) {
+ onError(PackageConfigFormatException(
+ 'Location URI must not have fragment', source, fragmentStart));
+ end = fragmentStart;
+ }
+ var packageValue = String.fromCharCodes(source, separatorIndex + 1, end);
+ Uri packageLocation;
+ try {
+ packageLocation = Uri.parse(packageValue);
+ } on FormatException catch (e) {
+ onError(PackageConfigFormatException.from(e));
+ continue;
+ }
+ var relativeRoot = !hasAbsolutePath(packageLocation);
+ packageLocation = baseLocation.resolveUri(packageLocation);
+ if (packageLocation.isScheme('package')) {
+ onError(PackageConfigFormatException(
+ 'Package URI as location for package', source, separatorIndex + 1));
+ continue;
+ }
+ var path = packageLocation.path;
+ if (!path.endsWith('/')) {
+ path += '/';
+ packageLocation = packageLocation.replace(path: path);
+ }
+ if (packageNames.contains(packageName)) {
+ onError(PackageConfigFormatException(
+ 'Same package name occurred more than once', source, start));
+ continue;
+ }
+ var rootUri = packageLocation;
+ if (path.endsWith('/lib/')) {
+ // Assume default Pub package layout. Include package itself in root.
+ rootUri =
+ packageLocation.replace(path: path.substring(0, path.length - 4));
+ }
+ var package = SimplePackage.validate(packageName, rootUri, packageLocation,
+ _languageVersion, null, relativeRoot, (error) {
+ if (error is ArgumentError) {
+ onError(PackageConfigFormatException(error.message.toString(), source));
+ } else {
+ onError(error);
+ }
+ });
+ if (package != null) {
+ packages.add(package);
+ packageNames.add(packageName);
+ }
+ }
+ return SimplePackageConfig(1, packages, null, onError);
+}
+
+/// Writes the configuration to a [StringSink].
+///
+/// If [comment] is provided, the output will contain this comment
+/// with `# ` in front of each line.
+/// Lines are defined as ending in line feed (`'\n'`). If the final
+/// line of the comment doesn't end in a line feed, one will be added.
+///
+/// If [baseUri] is provided, package locations will be made relative
+/// to the base URI, if possible, before writing.
+void write(StringSink output, PackageConfig config,
+ {Uri? baseUri, String? comment}) {
+ if (baseUri != null && !baseUri.isAbsolute) {
+ throw PackageConfigArgumentError(baseUri, 'baseUri', 'Must be absolute');
+ }
+
+ if (comment != null) {
+ var lines = comment.split('\n');
+ if (lines.last.isEmpty) lines.removeLast();
+ for (var commentLine in lines) {
+ output.write('# ');
+ output.writeln(commentLine);
+ }
+ } else {
+ output.write('# generated by package:package_config at ');
+ output.write(DateTime.now());
+ output.writeln();
+ }
+ for (var package in config.packages) {
+ var packageName = package.name;
+ var uri = package.packageUriRoot;
+ // Validate packageName.
+ if (!isValidPackageName(packageName)) {
+ throw PackageConfigArgumentError(
+ config, 'config', '"$packageName" is not a valid package name');
+ }
+ if (uri.scheme == 'package') {
+ throw PackageConfigArgumentError(
+ config, 'config', 'Package location must not be a package URI: $uri');
+ }
+ output.write(packageName);
+ output.write(':');
+ // If baseUri is provided, make the URI relative to baseUri.
+ if (baseUri != null) {
+ uri = relativizeUri(uri, baseUri)!;
+ }
+ if (!uri.path.endsWith('/')) {
+ uri = uri.replace(path: '${uri.path}/');
+ }
+ output.write(uri);
+ output.writeln();
+ }
+}
diff --git a/pkgs/package_config/lib/src/util.dart b/pkgs/package_config/lib/src/util.dart
new file mode 100644
index 0000000..4f0210c
--- /dev/null
+++ b/pkgs/package_config/lib/src/util.dart
@@ -0,0 +1,253 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Utility methods used by more than one library in the package.
+library;
+
+import 'errors.dart';
+
+// All ASCII characters that are valid in a package name, with space
+// for all the invalid ones (including space).
+const String _validPackageNameCharacters =
+ r" ! $ &'()*+,-. 0123456789 ; = "
+ r'@ABCDEFGHIJKLMNOPQRSTUVWXYZ _ abcdefghijklmnopqrstuvwxyz ~ ';
+
+/// Tests whether something is a valid Dart package name.
+bool isValidPackageName(String string) {
+ return checkPackageName(string) < 0;
+}
+
+/// Check if a string is a valid package name.
+///
+/// Valid package names contain only characters in [_validPackageNameCharacters]
+/// and must contain at least one non-'.' character.
+///
+/// Returns `-1` if the string is valid.
+/// Otherwise returns the index of the first invalid character,
+/// or `string.length` if the string contains no non-'.' character.
+int checkPackageName(String string) {
+ // Becomes non-zero if any non-'.' character is encountered.
+ var nonDot = 0;
+ for (var i = 0; i < string.length; i++) {
+ var c = string.codeUnitAt(i);
+ if (c > 0x7f || _validPackageNameCharacters.codeUnitAt(c) <= $space) {
+ return i;
+ }
+ nonDot += c ^ $dot;
+ }
+ if (nonDot == 0) return string.length;
+ return -1;
+}
+
+/// Validate that a [Uri] is a valid `package:` URI.
+///
+/// Used to validate user input.
+///
+/// Returns the package name extracted from the package URI,
+/// which is the path segment between `package:` and the first `/`.
+String checkValidPackageUri(Uri packageUri, String name) {
+ if (packageUri.scheme != 'package') {
+ throw PackageConfigArgumentError(packageUri, name, 'Not a package: URI');
+ }
+ if (packageUri.hasAuthority) {
+ throw PackageConfigArgumentError(
+ packageUri, name, 'Package URIs must not have a host part');
+ }
+ if (packageUri.hasQuery) {
+ // A query makes no sense if resolved to a file: URI.
+ throw PackageConfigArgumentError(
+ packageUri, name, 'Package URIs must not have a query part');
+ }
+ if (packageUri.hasFragment) {
+ // We could leave the fragment after the URL when resolving,
+ // but it would be odd if "package:foo/foo.dart#1" and
+ // "package:foo/foo.dart#2" were considered different libraries.
+ // Keep the syntax open in case we ever get multiple libraries in one file.
+ throw PackageConfigArgumentError(
+ packageUri, name, 'Package URIs must not have a fragment part');
+ }
+ if (packageUri.path.startsWith('/')) {
+ throw PackageConfigArgumentError(
+ packageUri, name, "Package URIs must not start with a '/'");
+ }
+ var firstSlash = packageUri.path.indexOf('/');
+ if (firstSlash == -1) {
+ throw PackageConfigArgumentError(packageUri, name,
+ "Package URIs must start with the package name followed by a '/'");
+ }
+ var packageName = packageUri.path.substring(0, firstSlash);
+ var badIndex = checkPackageName(packageName);
+ if (badIndex >= 0) {
+ if (packageName.isEmpty) {
+ throw PackageConfigArgumentError(
+ packageUri, name, 'Package names mus be non-empty');
+ }
+ if (badIndex == packageName.length) {
+ throw PackageConfigArgumentError(packageUri, name,
+ "Package names must contain at least one non-'.' character");
+ }
+ assert(badIndex < packageName.length);
+ var badCharCode = packageName.codeUnitAt(badIndex);
+ var badChar = 'U+${badCharCode.toRadixString(16).padLeft(4, '0')}';
+ if (badCharCode >= 0x20 && badCharCode <= 0x7e) {
+ // Printable character.
+ badChar = "'${packageName[badIndex]}' ($badChar)";
+ }
+ throw PackageConfigArgumentError(
+ packageUri, name, 'Package names must not contain $badChar');
+ }
+ return packageName;
+}
+
+/// Checks whether URI is just an absolute directory.
+///
+/// * It must have a scheme.
+/// * It must not have a query or fragment.
+/// * The path must end with `/`.
+bool isAbsoluteDirectoryUri(Uri uri) {
+ if (uri.hasQuery) return false;
+ if (uri.hasFragment) return false;
+ if (!uri.hasScheme) return false;
+ var path = uri.path;
+ if (!path.endsWith('/')) return false;
+ return true;
+}
+
+/// Whether the former URI is a prefix of the latter.
+bool isUriPrefix(Uri prefix, Uri path) {
+ assert(!prefix.hasFragment);
+ assert(!prefix.hasQuery);
+ assert(!path.hasQuery);
+ assert(!path.hasFragment);
+ assert(prefix.path.endsWith('/'));
+ return path.toString().startsWith(prefix.toString());
+}
+
+/// Finds the first non-JSON-whitespace character in a file.
+///
+/// Used to heuristically detect whether a file is a JSON file or an .ini file.
+int firstNonWhitespaceChar(List<int> bytes) {
+ for (var i = 0; i < bytes.length; i++) {
+ var char = bytes[i];
+ if (char != 0x20 && char != 0x09 && char != 0x0a && char != 0x0d) {
+ return char;
+ }
+ }
+ return -1;
+}
+
+/// Appends a trailing `/` if the path doesn't end with one.
+String trailingSlash(String path) {
+ if (path.isEmpty || path.endsWith('/')) return path;
+ return '$path/';
+}
+
+/// Whether a URI should not be considered relative to the base URI.
+///
+/// Used to determine whether a parsed root URI is relative
+/// to the configuration file or not.
+/// If it is relative, then it's rewritten as relative when
+/// output again later. If not, it's output as absolute.
+bool hasAbsolutePath(Uri uri) =>
+ uri.hasScheme || uri.hasAuthority || uri.hasAbsolutePath;
+
+/// Attempts to return a relative path-only URI for [uri].
+///
+/// First removes any query or fragment part from [uri].
+///
+/// If [uri] is already relative (has no scheme), it's returned as-is.
+/// If that is not desired, the caller can pass `baseUri.resolveUri(uri)`
+/// as the [uri] instead.
+///
+/// If the [uri] has a scheme or authority part which differs from
+/// the [baseUri], or if there is no overlap in the paths of the
+/// two URIs at all, the [uri] is returned as-is.
+///
+/// Otherwise the result is a path-only URI which satisfies
+/// `baseUri.resolveUri(result) == uri`,
+///
+/// The `baseUri` must be absolute.
+Uri? relativizeUri(Uri? uri, Uri? baseUri) {
+ if (baseUri == null) return uri;
+ assert(baseUri.isAbsolute);
+ if (uri!.hasQuery || uri.hasFragment) {
+ uri = Uri(
+ scheme: uri.scheme,
+ userInfo: uri.hasAuthority ? uri.userInfo : null,
+ host: uri.hasAuthority ? uri.host : null,
+ port: uri.hasAuthority ? uri.port : null,
+ path: uri.path);
+ }
+
+ // Already relative. We assume the caller knows what they are doing.
+ if (!uri.isAbsolute) return uri;
+
+ if (baseUri.scheme != uri.scheme) {
+ return uri;
+ }
+
+ // If authority differs, we could remove the scheme, but it's not worth it.
+ if (uri.hasAuthority != baseUri.hasAuthority) return uri;
+ if (uri.hasAuthority) {
+ if (uri.userInfo != baseUri.userInfo ||
+ uri.host.toLowerCase() != baseUri.host.toLowerCase() ||
+ uri.port != baseUri.port) {
+ return uri;
+ }
+ }
+
+ baseUri = baseUri.normalizePath();
+ var base = [...baseUri.pathSegments];
+ if (base.isNotEmpty) base.removeLast();
+ uri = uri.normalizePath();
+ var target = [...uri.pathSegments];
+ if (target.isNotEmpty && target.last.isEmpty) target.removeLast();
+ var index = 0;
+ while (index < base.length && index < target.length) {
+ if (base[index] != target[index]) {
+ break;
+ }
+ index++;
+ }
+ if (index == base.length) {
+ if (index == target.length) {
+ return Uri(path: './');
+ }
+ return Uri(path: target.skip(index).join('/'));
+ } else if (index > 0) {
+ var buffer = StringBuffer();
+ for (var n = base.length - index; n > 0; --n) {
+ buffer.write('../');
+ }
+ buffer.writeAll(target.skip(index), '/');
+ return Uri(path: buffer.toString());
+ } else {
+ return uri;
+ }
+}
+
+// Character constants used by this package.
+/// "Line feed" control character.
+const int $lf = 0x0a;
+
+/// "Carriage return" control character.
+const int $cr = 0x0d;
+
+/// Space character.
+const int $space = 0x20;
+
+/// Character `#`.
+const int $hash = 0x23;
+
+/// Character `.`.
+const int $dot = 0x2e;
+
+/// Character `:`.
+const int $colon = 0x3a;
+
+/// Character `?`.
+const int $question = 0x3f;
+
+/// Character `{`.
+const int $lbrace = 0x7b;
diff --git a/pkgs/package_config/lib/src/util_io.dart b/pkgs/package_config/lib/src/util_io.dart
new file mode 100644
index 0000000..4680eef
--- /dev/null
+++ b/pkgs/package_config/lib/src/util_io.dart
@@ -0,0 +1,108 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Utility methods requiring dart:io and used by more than one library in the
+/// package.
+library;
+
+import 'dart:io';
+import 'dart:typed_data';
+
+Future<Uint8List?> defaultLoader(Uri uri) async {
+ if (uri.isScheme('file')) {
+ var file = File.fromUri(uri);
+ try {
+ return await file.readAsBytes();
+ } catch (_) {
+ return null;
+ }
+ }
+ if (uri.isScheme('http') || uri.isScheme('https')) {
+ return _httpGet(uri);
+ }
+ throw UnsupportedError('Default URI unsupported scheme: $uri');
+}
+
+Future<Uint8List?> _httpGet(Uri uri) async {
+ assert(uri.isScheme('http') || uri.isScheme('https'));
+ var client = HttpClient();
+ var request = await client.getUrl(uri);
+ var response = await request.close();
+ if (response.statusCode != HttpStatus.ok) {
+ return null;
+ }
+ var splitContent = await response.toList();
+ var totalLength = 0;
+ if (splitContent.length == 1) {
+ var part = splitContent[0];
+ if (part is Uint8List) {
+ return part;
+ }
+ }
+ for (var list in splitContent) {
+ totalLength += list.length;
+ }
+ var result = Uint8List(totalLength);
+ var offset = 0;
+ for (var contentPart in splitContent as Iterable<Uint8List>) {
+ result.setRange(offset, offset + contentPart.length, contentPart);
+ offset += contentPart.length;
+ }
+ return result;
+}
+
+/// The file name of a path.
+///
+/// The file name is everything after the last occurrence of
+/// [Platform.pathSeparator], or the entire string if no
+/// path separator occurs in the string.
+String fileName(String path) {
+ var separator = Platform.pathSeparator;
+ var lastSeparator = path.lastIndexOf(separator);
+ if (lastSeparator < 0) return path;
+ return path.substring(lastSeparator + separator.length);
+}
+
+/// The directory name of a path.
+///
+/// The directory name is everything before the last occurrence of
+/// [Platform.pathSeparator], or the empty string if no
+/// path separator occurs in the string.
+String dirName(String path) {
+ var separator = Platform.pathSeparator;
+ var lastSeparator = path.lastIndexOf(separator);
+ if (lastSeparator < 0) return '';
+ return path.substring(0, lastSeparator);
+}
+
+/// Join path parts with the [Platform.pathSeparator].
+///
+/// If a part ends with a path separator, then no extra separator is
+/// inserted.
+String pathJoin(String part1, String part2, [String? part3]) {
+ var separator = Platform.pathSeparator;
+ var separator1 = part1.endsWith(separator) ? '' : separator;
+ if (part3 == null) {
+ return '$part1$separator1$part2';
+ }
+ var separator2 = part2.endsWith(separator) ? '' : separator;
+ return '$part1$separator1$part2$separator2$part3';
+}
+
+/// Join an unknown number of path parts with [Platform.pathSeparator].
+///
+/// If a part ends with a path separator, then no extra separator is
+/// inserted.
+String pathJoinAll(Iterable<String> parts) {
+ var buffer = StringBuffer();
+ var separator = '';
+ for (var part in parts) {
+ buffer
+ ..write(separator)
+ ..write(part);
+ separator =
+ part.endsWith(Platform.pathSeparator) ? '' : Platform.pathSeparator;
+ }
+ return buffer.toString();
+}
diff --git a/pkgs/package_config/pubspec.yaml b/pkgs/package_config/pubspec.yaml
new file mode 100644
index 0000000..28f3e13
--- /dev/null
+++ b/pkgs/package_config/pubspec.yaml
@@ -0,0 +1,14 @@
+name: package_config
+version: 2.1.1
+description: Support for reading and writing Dart Package Configuration files.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/package_config
+
+environment:
+ sdk: ^3.4.0
+
+dependencies:
+ path: ^1.8.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
+ test: ^1.16.0
diff --git a/pkgs/package_config/test/bench.dart b/pkgs/package_config/test/bench.dart
new file mode 100644
index 0000000..8428481
--- /dev/null
+++ b/pkgs/package_config/test/bench.dart
@@ -0,0 +1,71 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:package_config/src/errors.dart';
+import 'package:package_config/src/package_config_json.dart';
+
+void bench(final int size, final bool doPrint) {
+ var sb = StringBuffer();
+ sb.writeln('{');
+ sb.writeln('"configVersion": 2,');
+ sb.writeln('"packages": [');
+ for (var i = 0; i < size; i++) {
+ if (i != 0) {
+ sb.writeln(',');
+ }
+ sb.writeln('{');
+ sb.writeln(' "name": "p_$i",');
+ sb.writeln(' "rootUri": "file:///p_$i/",');
+ sb.writeln(' "packageUri": "lib/",');
+ sb.writeln(' "languageVersion": "2.5",');
+ sb.writeln(' "nonstandard": true');
+ sb.writeln('}');
+ }
+ sb.writeln('],');
+ sb.writeln('"generator": "pub",');
+ sb.writeln('"other": [42]');
+ sb.writeln('}');
+ var stopwatch = Stopwatch()..start();
+ var config = parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ utf8.encode(sb.toString()) as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'),
+ throwError,
+ );
+ final read = stopwatch.elapsedMilliseconds;
+
+ stopwatch.reset();
+ for (var i = 0; i < size; i++) {
+ if (config.packageOf(Uri.parse('file:///p_$i/lib/src/foo.dart'))!.name !=
+ 'p_$i') {
+ throw StateError('Unexpected result!');
+ }
+ }
+ final lookup = stopwatch.elapsedMilliseconds;
+
+ if (doPrint) {
+ print('Read file with $size packages in $read ms, '
+ 'looked up all packages in $lookup ms');
+ }
+}
+
+void main(List<String> args) {
+ if (args.length != 1 && args.length != 2) {
+ throw ArgumentError('Expects arguments: <size> <warmup iterations>?');
+ }
+ final size = int.parse(args[0]);
+ if (args.length > 1) {
+ final warmups = int.parse(args[1]);
+ print('Performing $warmups warmup iterations.');
+ for (var i = 0; i < warmups; i++) {
+ bench(10, false);
+ }
+ }
+
+ // Benchmark.
+ bench(size, true);
+}
diff --git a/pkgs/package_config/test/discovery_test.dart b/pkgs/package_config/test/discovery_test.dart
new file mode 100644
index 0000000..6d1b655
--- /dev/null
+++ b/pkgs/package_config/test/discovery_test.dart
@@ -0,0 +1,346 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+library;
+
+import 'dart:io';
+
+import 'package:package_config/package_config.dart';
+import 'package:test/test.dart';
+
+import 'src/util.dart';
+import 'src/util_io.dart';
+
+const packagesFile = '''
+# A comment
+foo:file:///dart/packages/foo/
+bar:/dart/packages/bar/
+baz:packages/baz/
+''';
+
+const packageConfigFile = '''
+{
+ "configVersion": 2,
+ "packages": [
+ {
+ "name": "foo",
+ "rootUri": "file:///dart/packages/foo/"
+ },
+ {
+ "name": "bar",
+ "rootUri": "/dart/packages/bar/"
+ },
+ {
+ "name": "baz",
+ "rootUri": "../packages/baz/"
+ }
+ ],
+ "extra": [42]
+}
+''';
+
+void validatePackagesFile(PackageConfig resolver, Directory directory) {
+ expect(resolver, isNotNull);
+ expect(resolver.resolve(pkg('foo', 'bar/baz')),
+ equals(Uri.parse('file:///dart/packages/foo/bar/baz')));
+ expect(resolver.resolve(pkg('bar', 'baz/qux')),
+ equals(Uri.parse('file:///dart/packages/bar/baz/qux')));
+ expect(resolver.resolve(pkg('baz', 'qux/foo')),
+ equals(Uri.directory(directory.path).resolve('packages/baz/qux/foo')));
+ expect([for (var p in resolver.packages) p.name],
+ unorderedEquals(['foo', 'bar', 'baz']));
+}
+
+void main() {
+ group('findPackages', () {
+ // Finds package_config.json if there.
+ fileTest('package_config.json', {
+ '.packages': 'invalid .packages file',
+ 'script.dart': 'main(){}',
+ 'packages': {'shouldNotBeFound': <Never, Never>{}},
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ }
+ }, (Directory directory) async {
+ var config = (await findPackageConfig(directory))!;
+ expect(config.version, 2); // Found package_config.json file.
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds .packages if no package_config.json.
+ fileTest('.packages', {
+ '.packages': packagesFile,
+ 'script.dart': 'main(){}',
+ 'packages': {'shouldNotBeFound': <Object, Object>{}}
+ }, (Directory directory) async {
+ var config = (await findPackageConfig(directory))!;
+ expect(config.version, 1); // Found .packages file.
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds package_config.json in super-directory.
+ fileTest('package_config.json recursive', {
+ '.packages': packagesFile,
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ 'subdir': {
+ 'script.dart': 'main(){}',
+ }
+ }, (Directory directory) async {
+ var config = (await findPackageConfig(subdir(directory, 'subdir/')))!;
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds .packages in super-directory.
+ fileTest('.packages recursive', {
+ '.packages': packagesFile,
+ 'subdir': {'script.dart': 'main(){}'}
+ }, (Directory directory) async {
+ var config = (await findPackageConfig(subdir(directory, 'subdir/')))!;
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ // Does not find a packages/ directory, and returns null if nothing found.
+ fileTest('package directory packages not supported', {
+ 'packages': {
+ 'foo': <String, dynamic>{},
+ }
+ }, (Directory directory) async {
+ var config = await findPackageConfig(directory);
+ expect(config, null);
+ });
+
+ group('throws', () {
+ fileTest('invalid .packages', {
+ '.packages': 'not a .packages file',
+ }, (Directory directory) {
+ expect(findPackageConfig(directory), throwsA(isA<FormatException>()));
+ });
+
+ fileTest('invalid .packages as JSON', {
+ '.packages': packageConfigFile,
+ }, (Directory directory) {
+ expect(findPackageConfig(directory), throwsA(isA<FormatException>()));
+ });
+
+ fileTest('invalid .packages', {
+ '.dart_tool': {
+ 'package_config.json': 'not a JSON file',
+ }
+ }, (Directory directory) {
+ expect(findPackageConfig(directory), throwsA(isA<FormatException>()));
+ });
+
+ fileTest('invalid .packages as INI', {
+ '.dart_tool': {
+ 'package_config.json': packagesFile,
+ }
+ }, (Directory directory) {
+ expect(findPackageConfig(directory), throwsA(isA<FormatException>()));
+ });
+ });
+
+ group('handles error', () {
+ fileTest('invalid .packages', {
+ '.packages': 'not a .packages file',
+ }, (Directory directory) async {
+ var hadError = false;
+ await findPackageConfig(directory,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<FormatException>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+
+ fileTest('invalid .packages as JSON', {
+ '.packages': packageConfigFile,
+ }, (Directory directory) async {
+ var hadError = false;
+ await findPackageConfig(directory,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<FormatException>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+
+ fileTest('invalid package_config not JSON', {
+ '.dart_tool': {
+ 'package_config.json': 'not a JSON file',
+ }
+ }, (Directory directory) async {
+ var hadError = false;
+ await findPackageConfig(directory,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<FormatException>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+
+ fileTest('invalid package config as INI', {
+ '.dart_tool': {
+ 'package_config.json': packagesFile,
+ }
+ }, (Directory directory) async {
+ var hadError = false;
+ await findPackageConfig(directory,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<FormatException>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+ });
+
+ // Does not find .packages if no package_config.json and minVersion > 1.
+ fileTest('.packages ignored', {
+ '.packages': packagesFile,
+ 'script.dart': 'main(){}'
+ }, (Directory directory) async {
+ var config = await findPackageConfig(directory, minVersion: 2);
+ expect(config, null);
+ });
+
+ // Finds package_config.json in super-directory, with .packages in
+ // subdir and minVersion > 1.
+ fileTest('package_config.json recursive .packages ignored', {
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ 'subdir': {
+ '.packages': packagesFile,
+ 'script.dart': 'main(){}',
+ }
+ }, (Directory directory) async {
+ var config = (await findPackageConfig(subdir(directory, 'subdir/'),
+ minVersion: 2))!;
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+ });
+
+ group('loadPackageConfig', () {
+ // Load a specific files
+ group('package_config.json', () {
+ var files = {
+ '.packages': packagesFile,
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ };
+ fileTest('directly', files, (Directory directory) async {
+ var file =
+ dirFile(subdir(directory, '.dart_tool'), 'package_config.json');
+ var config = await loadPackageConfig(file);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+ fileTest('indirectly through .packages', files,
+ (Directory directory) async {
+ var file = dirFile(directory, '.packages');
+ var config = await loadPackageConfig(file);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+ fileTest('prefer .packages', files, (Directory directory) async {
+ var file = dirFile(directory, '.packages');
+ var config = await loadPackageConfig(file, preferNewest: false);
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+ });
+
+ fileTest('package_config.json non-default name', {
+ '.packages': packagesFile,
+ 'subdir': {
+ 'pheldagriff': packageConfigFile,
+ },
+ }, (Directory directory) async {
+ var file = dirFile(directory, 'subdir/pheldagriff');
+ var config = await loadPackageConfig(file);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ fileTest('package_config.json named .packages', {
+ 'subdir': {
+ '.packages': packageConfigFile,
+ },
+ }, (Directory directory) async {
+ var file = dirFile(directory, 'subdir/.packages');
+ var config = await loadPackageConfig(file);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ fileTest('.packages', {
+ '.packages': packagesFile,
+ }, (Directory directory) async {
+ var file = dirFile(directory, '.packages');
+ var config = await loadPackageConfig(file);
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ fileTest('.packages non-default name', {
+ 'pheldagriff': packagesFile,
+ }, (Directory directory) async {
+ var file = dirFile(directory, 'pheldagriff');
+ var config = await loadPackageConfig(file);
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ fileTest('no config found', {}, (Directory directory) {
+ var file = dirFile(directory, 'anyname');
+ expect(
+ () => loadPackageConfig(file), throwsA(isA<FileSystemException>()));
+ });
+
+ fileTest('no config found, handled', {}, (Directory directory) async {
+ var file = dirFile(directory, 'anyname');
+ var hadError = false;
+ await loadPackageConfig(file,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<FileSystemException>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+
+ fileTest('specified file syntax error', {
+ 'anyname': 'syntax error',
+ }, (Directory directory) {
+ var file = dirFile(directory, 'anyname');
+ expect(() => loadPackageConfig(file), throwsFormatException);
+ });
+
+ // Find package_config.json in subdir even if initial file syntax error.
+ fileTest('specified file syntax onError', {
+ '.packages': 'syntax error',
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ }, (Directory directory) async {
+ var file = dirFile(directory, '.packages');
+ var config = await loadPackageConfig(file);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ // A file starting with `{` is a package_config.json file.
+ fileTest('file syntax error with {', {
+ '.packages': '{syntax error',
+ }, (Directory directory) {
+ var file = dirFile(directory, '.packages');
+ expect(() => loadPackageConfig(file), throwsFormatException);
+ });
+ });
+}
diff --git a/pkgs/package_config/test/discovery_uri_test.dart b/pkgs/package_config/test/discovery_uri_test.dart
new file mode 100644
index 0000000..542bf0a
--- /dev/null
+++ b/pkgs/package_config/test/discovery_uri_test.dart
@@ -0,0 +1,310 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+library;
+
+import 'package:package_config/package_config.dart';
+import 'package:test/test.dart';
+
+import 'src/util.dart';
+
+const packagesFile = '''
+# A comment
+foo:file:///dart/packages/foo/
+bar:/dart/packages/bar/
+baz:packages/baz/
+''';
+
+const packageConfigFile = '''
+{
+ "configVersion": 2,
+ "packages": [
+ {
+ "name": "foo",
+ "rootUri": "file:///dart/packages/foo/"
+ },
+ {
+ "name": "bar",
+ "rootUri": "/dart/packages/bar/"
+ },
+ {
+ "name": "baz",
+ "rootUri": "../packages/baz/"
+ }
+ ],
+ "extra": [42]
+}
+''';
+
+void validatePackagesFile(PackageConfig resolver, Uri directory) {
+ expect(resolver, isNotNull);
+ expect(resolver.resolve(pkg('foo', 'bar/baz')),
+ equals(Uri.parse('file:///dart/packages/foo/bar/baz')));
+ expect(resolver.resolve(pkg('bar', 'baz/qux')),
+ equals(directory.resolve('/dart/packages/bar/baz/qux')));
+ expect(resolver.resolve(pkg('baz', 'qux/foo')),
+ equals(directory.resolve('packages/baz/qux/foo')));
+ expect([for (var p in resolver.packages) p.name],
+ unorderedEquals(['foo', 'bar', 'baz']));
+}
+
+void main() {
+ group('findPackages', () {
+ // Finds package_config.json if there.
+ loaderTest('package_config.json', {
+ '.packages': 'invalid .packages file',
+ 'script.dart': 'main(){}',
+ 'packages': {'shouldNotBeFound': <String, dynamic>{}},
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ }
+ }, (directory, loader) async {
+ var config = (await findPackageConfigUri(directory, loader: loader))!;
+ expect(config.version, 2); // Found package_config.json file.
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds .packages if no package_config.json.
+ loaderTest('.packages', {
+ '.packages': packagesFile,
+ 'script.dart': 'main(){}',
+ 'packages': {'shouldNotBeFound': <String, dynamic>{}}
+ }, (directory, loader) async {
+ var config = (await findPackageConfigUri(directory, loader: loader))!;
+ expect(config.version, 1); // Found .packages file.
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds package_config.json in super-directory.
+ loaderTest('package_config.json recursive', {
+ '.packages': packagesFile,
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ 'subdir': {
+ 'script.dart': 'main(){}',
+ }
+ }, (directory, loader) async {
+ var config = (await findPackageConfigUri(directory.resolve('subdir/'),
+ loader: loader))!;
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ // Finds .packages in super-directory.
+ loaderTest('.packages recursive', {
+ '.packages': packagesFile,
+ 'subdir': {'script.dart': 'main(){}'}
+ }, (directory, loader) async {
+ var config = (await findPackageConfigUri(directory.resolve('subdir/'),
+ loader: loader))!;
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ // Does not find a packages/ directory, and returns null if nothing found.
+ loaderTest('package directory packages not supported', {
+ 'packages': {
+ 'foo': <String, dynamic>{},
+ }
+ }, (Uri directory, loader) async {
+ var config = await findPackageConfigUri(directory, loader: loader);
+ expect(config, null);
+ });
+
+ loaderTest('invalid .packages', {
+ '.packages': 'not a .packages file',
+ }, (Uri directory, loader) {
+ expect(() => findPackageConfigUri(directory, loader: loader),
+ throwsA(isA<FormatException>()));
+ });
+
+ loaderTest('invalid .packages as JSON', {
+ '.packages': packageConfigFile,
+ }, (Uri directory, loader) {
+ expect(() => findPackageConfigUri(directory, loader: loader),
+ throwsA(isA<FormatException>()));
+ });
+
+ loaderTest('invalid .packages', {
+ '.dart_tool': {
+ 'package_config.json': 'not a JSON file',
+ }
+ }, (Uri directory, loader) {
+ expect(() => findPackageConfigUri(directory, loader: loader),
+ throwsA(isA<FormatException>()));
+ });
+
+ loaderTest('invalid .packages as INI', {
+ '.dart_tool': {
+ 'package_config.json': packagesFile,
+ }
+ }, (Uri directory, loader) {
+ expect(() => findPackageConfigUri(directory, loader: loader),
+ throwsA(isA<FormatException>()));
+ });
+
+ // Does not find .packages if no package_config.json and minVersion > 1.
+ loaderTest('.packages ignored', {
+ '.packages': packagesFile,
+ 'script.dart': 'main(){}'
+ }, (directory, loader) async {
+ var config =
+ await findPackageConfigUri(directory, minVersion: 2, loader: loader);
+ expect(config, null);
+ });
+
+ // Finds package_config.json in super-directory, with .packages in
+ // subdir and minVersion > 1.
+ loaderTest('package_config.json recursive ignores .packages', {
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ 'subdir': {
+ '.packages': packagesFile,
+ 'script.dart': 'main(){}',
+ }
+ }, (directory, loader) async {
+ var config = (await findPackageConfigUri(directory.resolve('subdir/'),
+ minVersion: 2, loader: loader))!;
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+ });
+
+ group('loadPackageConfig', () {
+ // Load a specific files
+ group('package_config.json', () {
+ var files = {
+ '.packages': packagesFile,
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ };
+ loaderTest('directly', files, (Uri directory, loader) async {
+ var file = directory.resolve('.dart_tool/package_config.json');
+ var config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+ loaderTest('indirectly through .packages', files,
+ (Uri directory, loader) async {
+ var file = directory.resolve('.packages');
+ var config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+ });
+
+ loaderTest('package_config.json non-default name', {
+ '.packages': packagesFile,
+ 'subdir': {
+ 'pheldagriff': packageConfigFile,
+ },
+ }, (Uri directory, loader) async {
+ var file = directory.resolve('subdir/pheldagriff');
+ var config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ loaderTest('package_config.json named .packages', {
+ 'subdir': {
+ '.packages': packageConfigFile,
+ },
+ }, (Uri directory, loader) async {
+ var file = directory.resolve('subdir/.packages');
+ var config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 2);
+ validatePackagesFile(config, directory);
+ });
+
+ loaderTest('.packages', {
+ '.packages': packagesFile,
+ }, (Uri directory, loader) async {
+ var file = directory.resolve('.packages');
+ var config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ loaderTest('.packages non-default name', {
+ 'pheldagriff': packagesFile,
+ }, (Uri directory, loader) async {
+ var file = directory.resolve('pheldagriff');
+ var config = await loadPackageConfigUri(file, loader: loader);
+ expect(config.version, 1);
+ validatePackagesFile(config, directory);
+ });
+
+ loaderTest('no config found', {}, (Uri directory, loader) {
+ var file = directory.resolve('anyname');
+ expect(() => loadPackageConfigUri(file, loader: loader),
+ throwsA(isA<ArgumentError>()));
+ });
+
+ loaderTest('no config found, handle error', {},
+ (Uri directory, loader) async {
+ var file = directory.resolve('anyname');
+ var hadError = false;
+ await loadPackageConfigUri(file,
+ loader: loader,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<ArgumentError>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+
+ loaderTest('specified file syntax error', {
+ 'anyname': 'syntax error',
+ }, (Uri directory, loader) {
+ var file = directory.resolve('anyname');
+ expect(() => loadPackageConfigUri(file, loader: loader),
+ throwsFormatException);
+ });
+
+ loaderTest('specified file syntax onError', {
+ 'anyname': 'syntax error',
+ }, (directory, loader) async {
+ var file = directory.resolve('anyname');
+ var hadError = false;
+ await loadPackageConfigUri(file,
+ loader: loader,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<FormatException>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+
+ // Don't look for package_config.json if original file not named .packages.
+ loaderTest('specified file syntax error with alternative', {
+ 'anyname': 'syntax error',
+ '.dart_tool': {
+ 'package_config.json': packageConfigFile,
+ },
+ }, (directory, loader) async {
+ var file = directory.resolve('anyname');
+ expect(() => loadPackageConfigUri(file, loader: loader),
+ throwsFormatException);
+ });
+
+ // A file starting with `{` is a package_config.json file.
+ loaderTest('file syntax error with {', {
+ '.packages': '{syntax error',
+ }, (directory, loader) async {
+ var file = directory.resolve('.packages');
+ var hadError = false;
+ await loadPackageConfigUri(file,
+ loader: loader,
+ onError: expectAsync1((error) {
+ hadError = true;
+ expect(error, isA<FormatException>());
+ }, max: -1));
+ expect(hadError, true);
+ });
+ });
+}
diff --git a/pkgs/package_config/test/package_config_impl_test.dart b/pkgs/package_config/test/package_config_impl_test.dart
new file mode 100644
index 0000000..0f39963
--- /dev/null
+++ b/pkgs/package_config/test/package_config_impl_test.dart
@@ -0,0 +1,188 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert' show jsonDecode;
+
+import 'package:package_config/package_config_types.dart';
+import 'package:test/test.dart';
+import 'src/util.dart';
+
+void main() {
+ var unique = Object();
+ var root = Uri.file('/tmp/root/');
+
+ group('LanguageVersion', () {
+ test('minimal', () {
+ var version = LanguageVersion(3, 5);
+ expect(version.major, 3);
+ expect(version.minor, 5);
+ });
+
+ test('negative major', () {
+ expect(() => LanguageVersion(-1, 1), throwsArgumentError);
+ });
+
+ test('negative minor', () {
+ expect(() => LanguageVersion(1, -1), throwsArgumentError);
+ });
+
+ test('minimal parse', () {
+ var version = LanguageVersion.parse('3.5');
+ expect(version.major, 3);
+ expect(version.minor, 5);
+ });
+
+ void failParse(String name, String input) {
+ test('$name - error', () {
+ expect(() => LanguageVersion.parse(input),
+ throwsA(isA<PackageConfigError>()));
+ expect(() => LanguageVersion.parse(input), throwsFormatException);
+ var failed = false;
+ var actual = LanguageVersion.parse(input, onError: (_) {
+ failed = true;
+ });
+ expect(failed, true);
+ expect(actual, isA<LanguageVersion>());
+ });
+ }
+
+ failParse('Leading zero major', '01.1');
+ failParse('Leading zero minor', '1.01');
+ failParse('Sign+ major', '+1.1');
+ failParse('Sign- major', '-1.1');
+ failParse('Sign+ minor', '1.+1');
+ failParse('Sign- minor', '1.-1');
+ failParse('WhiteSpace 1', ' 1.1');
+ failParse('WhiteSpace 2', '1 .1');
+ failParse('WhiteSpace 3', '1. 1');
+ failParse('WhiteSpace 4', '1.1 ');
+ });
+
+ group('Package', () {
+ test('minimal', () {
+ var package = Package('name', root, extraData: unique);
+ expect(package.name, 'name');
+ expect(package.root, root);
+ expect(package.packageUriRoot, root);
+ expect(package.languageVersion, null);
+ expect(package.extraData, same(unique));
+ });
+
+ test('absolute package root', () {
+ var version = LanguageVersion(1, 1);
+ var absolute = root.resolve('foo/bar/');
+ var package = Package('name', root,
+ packageUriRoot: absolute,
+ relativeRoot: false,
+ languageVersion: version,
+ extraData: unique);
+ expect(package.name, 'name');
+ expect(package.root, root);
+ expect(package.packageUriRoot, absolute);
+ expect(package.languageVersion, version);
+ expect(package.extraData, same(unique));
+ expect(package.relativeRoot, false);
+ });
+
+ test('relative package root', () {
+ var relative = Uri.parse('foo/bar/');
+ var absolute = root.resolveUri(relative);
+ var package = Package('name', root,
+ packageUriRoot: relative, relativeRoot: true, extraData: unique);
+ expect(package.name, 'name');
+ expect(package.root, root);
+ expect(package.packageUriRoot, absolute);
+ expect(package.relativeRoot, true);
+ expect(package.languageVersion, null);
+ expect(package.extraData, same(unique));
+ });
+
+ for (var badName in ['a/z', 'a:z', '', '...']) {
+ test("Invalid name '$badName'", () {
+ expect(() => Package(badName, root), throwsPackageConfigError);
+ });
+ }
+
+ test('Invalid root, not absolute', () {
+ expect(
+ () => Package('name', Uri.parse('/foo/')), throwsPackageConfigError);
+ });
+
+ test('Invalid root, not ending in slash', () {
+ expect(() => Package('name', Uri.parse('file:///foo')),
+ throwsPackageConfigError);
+ });
+
+ test('invalid package root, not ending in slash', () {
+ expect(() => Package('name', root, packageUriRoot: Uri.parse('foo')),
+ throwsPackageConfigError);
+ });
+
+ test('invalid package root, not inside root', () {
+ expect(() => Package('name', root, packageUriRoot: Uri.parse('../baz/')),
+ throwsPackageConfigError);
+ });
+ });
+
+ group('package config', () {
+ test('empty', () {
+ var empty = PackageConfig([], extraData: unique);
+ expect(empty.version, 2);
+ expect(empty.packages, isEmpty);
+ expect(empty.extraData, same(unique));
+ expect(empty.resolve(pkg('a', 'b')), isNull);
+ });
+
+ test('single', () {
+ var package = Package('name', root);
+ var single = PackageConfig([package], extraData: unique);
+ expect(single.version, 2);
+ expect(single.packages, hasLength(1));
+ expect(single.extraData, same(unique));
+ expect(single.resolve(pkg('a', 'b')), isNull);
+ var resolved = single.resolve(pkg('name', 'a/b'));
+ expect(resolved, root.resolve('a/b'));
+ });
+ });
+ test('writeString', () {
+ var config = PackageConfig([
+ Package('foo', Uri.parse('file:///pkg/foo/'),
+ packageUriRoot: Uri.parse('file:///pkg/foo/lib/'),
+ relativeRoot: false,
+ languageVersion: LanguageVersion(2, 4),
+ extraData: {'foo': 'foo!'}),
+ Package('bar', Uri.parse('file:///pkg/bar/'),
+ packageUriRoot: Uri.parse('file:///pkg/bar/lib/'),
+ relativeRoot: true,
+ extraData: {'bar': 'bar!'}),
+ ], extraData: {
+ 'extra': 'data'
+ });
+ var buffer = StringBuffer();
+ PackageConfig.writeString(config, buffer, Uri.parse('file:///pkg/'));
+ var text = buffer.toString();
+ var json = jsonDecode(text); // Is valid JSON.
+ expect(json, {
+ 'configVersion': 2,
+ 'packages': unorderedEquals([
+ {
+ 'name': 'foo',
+ 'rootUri': 'file:///pkg/foo/',
+ 'packageUri': 'lib/',
+ 'languageVersion': '2.4',
+ 'foo': 'foo!',
+ },
+ {
+ 'name': 'bar',
+ 'rootUri': 'bar/',
+ 'packageUri': 'lib/',
+ 'bar': 'bar!',
+ },
+ ]),
+ 'extra': 'data',
+ });
+ });
+}
+
+final Matcher throwsPackageConfigError = throwsA(isA<PackageConfigError>());
diff --git a/pkgs/package_config/test/parse_test.dart b/pkgs/package_config/test/parse_test.dart
new file mode 100644
index 0000000..a92b9bf
--- /dev/null
+++ b/pkgs/package_config/test/parse_test.dart
@@ -0,0 +1,552 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:package_config/package_config_types.dart';
+import 'package:package_config/src/errors.dart';
+import 'package:package_config/src/package_config_json.dart';
+import 'package:package_config/src/packages_file.dart' as packages;
+import 'package:test/test.dart';
+
+import 'src/util.dart';
+
+void main() {
+ group('.packages', () {
+ test('valid', () {
+ var packagesFile = '# Generated by pub yadda yadda\n'
+ 'foo:file:///foo/lib/\n'
+ 'bar:/bar/lib/\n'
+ 'baz:lib/\n';
+ var result = packages.parse(utf8.encode(packagesFile),
+ Uri.parse('file:///tmp/file.dart'), throwError);
+ expect(result.version, 1);
+ expect({for (var p in result.packages) p.name}, {'foo', 'bar', 'baz'});
+ expect(result.resolve(pkg('foo', 'foo.dart')),
+ Uri.parse('file:///foo/lib/foo.dart'));
+ expect(result.resolve(pkg('bar', 'bar.dart')),
+ Uri.parse('file:///bar/lib/bar.dart'));
+ expect(result.resolve(pkg('baz', 'baz.dart')),
+ Uri.parse('file:///tmp/lib/baz.dart'));
+
+ var foo = result['foo']!;
+ expect(foo, isNotNull);
+ expect(foo.root, Uri.parse('file:///foo/'));
+ expect(foo.packageUriRoot, Uri.parse('file:///foo/lib/'));
+ expect(foo.languageVersion, LanguageVersion(2, 7));
+ expect(foo.relativeRoot, false);
+ });
+
+ test('valid empty', () {
+ var packagesFile = '# Generated by pub yadda yadda\n';
+ var result = packages.parse(
+ utf8.encode(packagesFile), Uri.file('/tmp/file.dart'), throwError);
+ expect(result.version, 1);
+ expect({for (var p in result.packages) p.name}, <String>{});
+ });
+
+ group('invalid', () {
+ var baseFile = Uri.file('/tmp/file.dart');
+ void testThrows(String name, String content) {
+ test(name, () {
+ expect(
+ () => packages.parse(utf8.encode(content), baseFile, throwError),
+ throwsA(isA<FormatException>()));
+ });
+ test('$name, handle error', () {
+ var hadError = false;
+ packages.parse(utf8.encode(content), baseFile, (error) {
+ hadError = true;
+ expect(error, isA<FormatException>());
+ });
+ expect(hadError, true);
+ });
+ }
+
+ testThrows('repeated package name', 'foo:lib/\nfoo:lib\n');
+ testThrows('no colon', 'foo\n');
+ testThrows('empty package name', ':lib/\n');
+ testThrows('dot only package name', '.:lib/\n');
+ testThrows('dot only package name', '..:lib/\n');
+ testThrows('invalid package name character', 'f\\o:lib/\n');
+ testThrows('package URI', 'foo:package:bar/lib/');
+ testThrows('location with query', 'f\\o:lib/?\n');
+ testThrows('location with fragment', 'f\\o:lib/#\n');
+ });
+ });
+
+ group('package_config.json', () {
+ test('valid', () {
+ var packageConfigFile = '''
+ {
+ "configVersion": 2,
+ "packages": [
+ {
+ "name": "foo",
+ "rootUri": "file:///foo/",
+ "packageUri": "lib/",
+ "languageVersion": "2.5",
+ "nonstandard": true
+ },
+ {
+ "name": "bar",
+ "rootUri": "/bar/",
+ "packageUri": "lib/",
+ "languageVersion": "9999.9999"
+ },
+ {
+ "name": "baz",
+ "rootUri": "../",
+ "packageUri": "lib/"
+ },
+ {
+ "name": "noslash",
+ "rootUri": "../noslash",
+ "packageUri": "lib"
+ }
+ ],
+ "generator": "pub",
+ "other": [42]
+ }
+ ''';
+ var config = parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ utf8.encode(packageConfigFile) as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'),
+ throwError);
+ expect(config.version, 2);
+ expect({for (var p in config.packages) p.name},
+ {'foo', 'bar', 'baz', 'noslash'});
+
+ expect(config.resolve(pkg('foo', 'foo.dart')),
+ Uri.parse('file:///foo/lib/foo.dart'));
+ expect(config.resolve(pkg('bar', 'bar.dart')),
+ Uri.parse('file:///bar/lib/bar.dart'));
+ expect(config.resolve(pkg('baz', 'baz.dart')),
+ Uri.parse('file:///tmp/lib/baz.dart'));
+
+ var foo = config['foo']!;
+ expect(foo, isNotNull);
+ expect(foo.root, Uri.parse('file:///foo/'));
+ expect(foo.packageUriRoot, Uri.parse('file:///foo/lib/'));
+ expect(foo.languageVersion, LanguageVersion(2, 5));
+ expect(foo.extraData, {'nonstandard': true});
+ expect(foo.relativeRoot, false);
+
+ var bar = config['bar']!;
+ expect(bar, isNotNull);
+ expect(bar.root, Uri.parse('file:///bar/'));
+ expect(bar.packageUriRoot, Uri.parse('file:///bar/lib/'));
+ expect(bar.languageVersion, LanguageVersion(9999, 9999));
+ expect(bar.extraData, null);
+ expect(bar.relativeRoot, false);
+
+ var baz = config['baz']!;
+ expect(baz, isNotNull);
+ expect(baz.root, Uri.parse('file:///tmp/'));
+ expect(baz.packageUriRoot, Uri.parse('file:///tmp/lib/'));
+ expect(baz.languageVersion, null);
+ expect(baz.relativeRoot, true);
+
+ // No slash after root or package root. One is inserted.
+ var noslash = config['noslash']!;
+ expect(noslash, isNotNull);
+ expect(noslash.root, Uri.parse('file:///tmp/noslash/'));
+ expect(noslash.packageUriRoot, Uri.parse('file:///tmp/noslash/lib/'));
+ expect(noslash.languageVersion, null);
+ expect(noslash.relativeRoot, true);
+
+ expect(config.extraData, {
+ 'generator': 'pub',
+ 'other': [42]
+ });
+ });
+
+ test('valid other order', () {
+ // The ordering in the file is not important.
+ var packageConfigFile = '''
+ {
+ "generator": "pub",
+ "other": [42],
+ "packages": [
+ {
+ "languageVersion": "2.5",
+ "packageUri": "lib/",
+ "rootUri": "file:///foo/",
+ "name": "foo"
+ },
+ {
+ "packageUri": "lib/",
+ "languageVersion": "9999.9999",
+ "rootUri": "/bar/",
+ "name": "bar"
+ },
+ {
+ "packageUri": "lib/",
+ "name": "baz",
+ "rootUri": "../"
+ }
+ ],
+ "configVersion": 2
+ }
+ ''';
+ var config = parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ utf8.encode(packageConfigFile) as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'),
+ throwError);
+ expect(config.version, 2);
+ expect({for (var p in config.packages) p.name}, {'foo', 'bar', 'baz'});
+
+ expect(config.resolve(pkg('foo', 'foo.dart')),
+ Uri.parse('file:///foo/lib/foo.dart'));
+ expect(config.resolve(pkg('bar', 'bar.dart')),
+ Uri.parse('file:///bar/lib/bar.dart'));
+ expect(config.resolve(pkg('baz', 'baz.dart')),
+ Uri.parse('file:///tmp/lib/baz.dart'));
+ expect(config.extraData, {
+ 'generator': 'pub',
+ 'other': [42]
+ });
+ });
+
+ // Check that a few minimal configurations are valid.
+ // These form the basis of invalid tests below.
+ var cfg = '"configVersion":2';
+ var pkgs = '"packages":[]';
+ var name = '"name":"foo"';
+ var root = '"rootUri":"/foo/"';
+ test('minimal', () {
+ var config = parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ utf8.encode('{$cfg,$pkgs}') as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'),
+ throwError);
+ expect(config.version, 2);
+ expect(config.packages, isEmpty);
+ });
+ test('minimal package', () {
+ // A package must have a name and a rootUri, the remaining properties
+ // are optional.
+ var config = parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ utf8.encode('{$cfg,"packages":[{$name,$root}]}') as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'),
+ throwError);
+ expect(config.version, 2);
+ expect(config.packages.first.name, 'foo');
+ });
+
+ test('nested packages', () {
+ var configBytes = utf8.encode(json.encode({
+ 'configVersion': 2,
+ 'packages': [
+ {'name': 'foo', 'rootUri': '/foo/', 'packageUri': 'lib/'},
+ {'name': 'bar', 'rootUri': '/foo/bar/', 'packageUri': 'lib/'},
+ {'name': 'baz', 'rootUri': '/foo/bar/baz/', 'packageUri': 'lib/'},
+ {'name': 'qux', 'rootUri': '/foo/qux/', 'packageUri': 'lib/'},
+ ]
+ }));
+ // ignore: unnecessary_cast
+ var config = parsePackageConfigBytes(configBytes as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError);
+ expect(config.version, 2);
+ expect(config.packageOf(Uri.parse('file:///foo/lala/lala.dart'))!.name,
+ 'foo');
+ expect(config.packageOf(Uri.parse('file:///foo/bar/lala.dart'))!.name,
+ 'bar');
+ expect(config.packageOf(Uri.parse('file:///foo/bar/baz/lala.dart'))!.name,
+ 'baz');
+ expect(config.packageOf(Uri.parse('file:///foo/qux/lala.dart'))!.name,
+ 'qux');
+ expect(config.toPackageUri(Uri.parse('file:///foo/lib/diz')),
+ Uri.parse('package:foo/diz'));
+ expect(config.toPackageUri(Uri.parse('file:///foo/bar/lib/diz')),
+ Uri.parse('package:bar/diz'));
+ expect(config.toPackageUri(Uri.parse('file:///foo/bar/baz/lib/diz')),
+ Uri.parse('package:baz/diz'));
+ expect(config.toPackageUri(Uri.parse('file:///foo/qux/lib/diz')),
+ Uri.parse('package:qux/diz'));
+ });
+
+ test('nested packages 2', () {
+ var configBytes = utf8.encode(json.encode({
+ 'configVersion': 2,
+ 'packages': [
+ {'name': 'foo', 'rootUri': '/', 'packageUri': 'lib/'},
+ {'name': 'bar', 'rootUri': '/bar/', 'packageUri': 'lib/'},
+ {'name': 'baz', 'rootUri': '/bar/baz/', 'packageUri': 'lib/'},
+ {'name': 'qux', 'rootUri': '/qux/', 'packageUri': 'lib/'},
+ ]
+ }));
+ // ignore: unnecessary_cast
+ var config = parsePackageConfigBytes(configBytes as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError);
+ expect(config.version, 2);
+ expect(
+ config.packageOf(Uri.parse('file:///lala/lala.dart'))!.name, 'foo');
+ expect(config.packageOf(Uri.parse('file:///bar/lala.dart'))!.name, 'bar');
+ expect(config.packageOf(Uri.parse('file:///bar/baz/lala.dart'))!.name,
+ 'baz');
+ expect(config.packageOf(Uri.parse('file:///qux/lala.dart'))!.name, 'qux');
+ expect(config.toPackageUri(Uri.parse('file:///lib/diz')),
+ Uri.parse('package:foo/diz'));
+ expect(config.toPackageUri(Uri.parse('file:///bar/lib/diz')),
+ Uri.parse('package:bar/diz'));
+ expect(config.toPackageUri(Uri.parse('file:///bar/baz/lib/diz')),
+ Uri.parse('package:baz/diz'));
+ expect(config.toPackageUri(Uri.parse('file:///qux/lib/diz')),
+ Uri.parse('package:qux/diz'));
+ });
+
+ test('packageOf is case sensitive on windows', () {
+ var configBytes = utf8.encode(json.encode({
+ 'configVersion': 2,
+ 'packages': [
+ {'name': 'foo', 'rootUri': 'file:///C:/Foo/', 'packageUri': 'lib/'},
+ ]
+ }));
+ var config = parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ configBytes as Uint8List,
+ Uri.parse('file:///C:/tmp/.dart_tool/file.dart'),
+ throwError);
+ expect(config.version, 2);
+ expect(
+ config.packageOf(Uri.parse('file:///C:/foo/lala/lala.dart')), null);
+ expect(config.packageOf(Uri.parse('file:///C:/Foo/lala/lala.dart'))!.name,
+ 'foo');
+ });
+
+ group('invalid', () {
+ void testThrows(String name, String source) {
+ test(name, () {
+ expect(
+ // ignore: unnecessary_cast
+ () => parsePackageConfigBytes(utf8.encode(source) as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'), throwError),
+ throwsA(isA<FormatException>()));
+ });
+ }
+
+ void testThrowsContains(
+ String name, String source, String containsString) {
+ test(name, () {
+ dynamic exception;
+ try {
+ parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ utf8.encode(source) as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'),
+ throwError,
+ );
+ } catch (e) {
+ exception = e;
+ }
+ if (exception == null) fail("Didn't get exception");
+ expect('$exception', contains(containsString));
+ });
+ }
+
+ testThrows('comment', '# comment\n {$cfg,$pkgs}');
+ testThrows('.packages file', 'foo:/foo\n');
+ testThrows('no configVersion', '{$pkgs}');
+ testThrows('no packages', '{$cfg}');
+ group('config version:', () {
+ testThrows('null', '{"configVersion":null,$pkgs}');
+ testThrows('string', '{"configVersion":"2",$pkgs}');
+ testThrows('array', '{"configVersion":[2],$pkgs}');
+ });
+ group('packages:', () {
+ testThrows('null', '{$cfg,"packages":null}');
+ testThrows('string', '{$cfg,"packages":"foo"}');
+ testThrows('object', '{$cfg,"packages":{}}');
+ });
+ group('packages entry:', () {
+ testThrows('null', '{$cfg,"packages":[null]}');
+ testThrows('string', '{$cfg,"packages":["foo"]}');
+ testThrows('array', '{$cfg,"packages":[[]]}');
+ });
+ group('package', () {
+ testThrows('no name', '{$cfg,"packages":[{$root}]}');
+ group('name:', () {
+ testThrows('null', '{$cfg,"packages":[{"name":null,$root}]}');
+ testThrows('num', '{$cfg,"packages":[{"name":1,$root}]}');
+ testThrows('object', '{$cfg,"packages":[{"name":{},$root}]}');
+ testThrows('empty', '{$cfg,"packages":[{"name":"",$root}]}');
+ testThrows('one-dot', '{$cfg,"packages":[{"name":".",$root}]}');
+ testThrows('two-dot', '{$cfg,"packages":[{"name":"..",$root}]}');
+ testThrows(
+ "invalid char '\\'", '{$cfg,"packages":[{"name":"\\",$root}]}');
+ testThrows(
+ "invalid char ':'", '{$cfg,"packages":[{"name":":",$root}]}');
+ testThrows(
+ "invalid char ' '", '{$cfg,"packages":[{"name":" ",$root}]}');
+ });
+
+ testThrows('no root', '{$cfg,"packages":[{$name}]}');
+ group('root:', () {
+ testThrows('null', '{$cfg,"packages":[{$name,"rootUri":null}]}');
+ testThrows('num', '{$cfg,"packages":[{$name,"rootUri":1}]}');
+ testThrows('object', '{$cfg,"packages":[{$name,"rootUri":{}}]}');
+ testThrows('fragment', '{$cfg,"packages":[{$name,"rootUri":"x/#"}]}');
+ testThrows('query', '{$cfg,"packages":[{$name,"rootUri":"x/?"}]}');
+ testThrows('package-URI',
+ '{$cfg,"packages":[{$name,"rootUri":"package:x/x/"}]}');
+ });
+ group('package-URI root:', () {
+ testThrows(
+ 'null', '{$cfg,"packages":[{$name,$root,"packageUri":null}]}');
+ testThrows('num', '{$cfg,"packages":[{$name,$root,"packageUri":1}]}');
+ testThrows(
+ 'object', '{$cfg,"packages":[{$name,$root,"packageUri":{}}]}');
+ testThrows('fragment',
+ '{$cfg,"packages":[{$name,$root,"packageUri":"x/#"}]}');
+ testThrows(
+ 'query', '{$cfg,"packages":[{$name,$root,"packageUri":"x/?"}]}');
+ testThrows('package: URI',
+ '{$cfg,"packages":[{$name,$root,"packageUri":"package:x/x/"}]}');
+ testThrows('not inside root',
+ '{$cfg,"packages":[{$name,$root,"packageUri":"../other/"}]}');
+ });
+ group('language version', () {
+ testThrows('null',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":null}]}');
+ testThrows(
+ 'num', '{$cfg,"packages":[{$name,$root,"languageVersion":1}]}');
+ testThrows('object',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":{}}]}');
+ testThrows('empty',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":""}]}');
+ testThrows('non number.number',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"x.1"}]}');
+ testThrows('number.non number',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1.x"}]}');
+ testThrows('non number',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"x"}]}');
+ testThrows('one number',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1"}]}');
+ testThrows('three numbers',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1.2.3"}]}');
+ testThrows('leading zero first',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"01.1"}]}');
+ testThrows('leading zero second',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1.01"}]}');
+ testThrows('trailing-',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1.1-1"}]}');
+ testThrows('trailing+',
+ '{$cfg,"packages":[{$name,$root,"languageVersion":"1.1+1"}]}');
+ });
+ });
+ testThrows('duplicate package name',
+ '{$cfg,"packages":[{$name,$root},{$name,"rootUri":"/other/"}]}');
+ testThrowsContains(
+ // The roots of foo and bar are the same.
+ 'same roots',
+ '{$cfg,"packages":[{$name,$root},{"name":"bar",$root}]}',
+ 'the same root directory');
+ testThrowsContains(
+ // The roots of foo and bar are the same.
+ 'same roots 2',
+ '{$cfg,"packages":[{$name,"rootUri":"/"},{"name":"bar","rootUri":"/"}]}',
+ 'the same root directory');
+ testThrowsContains(
+ // The root of bar is inside the root of foo,
+ // but the package root of foo is inside the root of bar.
+ 'between root and lib',
+ '{$cfg,"packages":['
+ '{"name":"foo","rootUri":"/foo/","packageUri":"bar/lib/"},'
+ '{"name":"bar","rootUri":"/foo/bar/","packageUri":"baz/lib"}]}',
+ 'package root of foo is inside the root of bar');
+
+ // This shouldn't be allowed, but for internal reasons it is.
+ test('package inside package root', () {
+ var config = parsePackageConfigBytes(
+ // ignore: unnecessary_cast
+ utf8.encode(
+ '{$cfg,"packages":['
+ '{"name":"foo","rootUri":"/foo/","packageUri":"lib/"},'
+ '{"name":"bar","rootUri":"/foo/lib/bar/","packageUri":"lib"}]}',
+ ) as Uint8List,
+ Uri.parse('file:///tmp/.dart_tool/file.dart'),
+ throwError);
+ expect(
+ config
+ .packageOf(Uri.parse('file:///foo/lib/bar/lib/lala.dart'))!
+ .name,
+ 'foo'); // why not bar?
+ expect(config.toPackageUri(Uri.parse('file:///foo/lib/bar/lib/diz')),
+ Uri.parse('package:foo/bar/lib/diz')); // why not package:bar/diz?
+ });
+ });
+ });
+
+ group('factories', () {
+ void testConfig(String name, PackageConfig config, PackageConfig expected) {
+ group(name, () {
+ test('structure', () {
+ expect(config.version, expected.version);
+ var expectedPackages = {for (var p in expected.packages) p.name};
+ var actualPackages = {for (var p in config.packages) p.name};
+ expect(actualPackages, expectedPackages);
+ });
+ for (var package in config.packages) {
+ var name = package.name;
+ test('package $name', () {
+ var expectedPackage = expected[name]!;
+ expect(expectedPackage, isNotNull);
+ expect(package.root, expectedPackage.root, reason: 'root');
+ expect(package.packageUriRoot, expectedPackage.packageUriRoot,
+ reason: 'package root');
+ expect(package.languageVersion, expectedPackage.languageVersion,
+ reason: 'languageVersion');
+ });
+ }
+ });
+ }
+
+ var configText = '''
+ {"configVersion": 2, "packages": [
+ {
+ "name": "foo",
+ "rootUri": "foo/",
+ "packageUri": "bar/",
+ "languageVersion": "1.2"
+ }
+ ]}
+ ''';
+ var baseUri = Uri.parse('file:///start/');
+ var config = PackageConfig([
+ Package('foo', Uri.parse('file:///start/foo/'),
+ packageUriRoot: Uri.parse('file:///start/foo/bar/'),
+ languageVersion: LanguageVersion(1, 2))
+ ]);
+ testConfig(
+ 'string', PackageConfig.parseString(configText, baseUri), config);
+ testConfig(
+ 'bytes',
+ PackageConfig.parseBytes(
+ Uint8List.fromList(configText.codeUnits), baseUri),
+ config);
+ testConfig('json', PackageConfig.parseJson(jsonDecode(configText), baseUri),
+ config);
+
+ baseUri = Uri.parse('file:///start2/');
+ config = PackageConfig([
+ Package('foo', Uri.parse('file:///start2/foo/'),
+ packageUriRoot: Uri.parse('file:///start2/foo/bar/'),
+ languageVersion: LanguageVersion(1, 2))
+ ]);
+ testConfig(
+ 'string2', PackageConfig.parseString(configText, baseUri), config);
+ testConfig(
+ 'bytes2',
+ PackageConfig.parseBytes(
+ Uint8List.fromList(configText.codeUnits), baseUri),
+ config);
+ testConfig('json2',
+ PackageConfig.parseJson(jsonDecode(configText), baseUri), config);
+ });
+}
diff --git a/pkgs/package_config/test/src/util.dart b/pkgs/package_config/test/src/util.dart
new file mode 100644
index 0000000..780ee80
--- /dev/null
+++ b/pkgs/package_config/test/src/util.dart
@@ -0,0 +1,57 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:test/test.dart';
+
+/// Creates a package: URI.
+Uri pkg(String packageName, String packagePath) {
+ var path =
+ "$packageName${packagePath.startsWith('/') ? "" : "/"}$packagePath";
+ return Uri(scheme: 'package', path: path);
+}
+
+// Remove if not used.
+String configFromPackages(List<List<String>> packages) => """
+{
+ "configVersion": 2,
+ "packages": [
+${packages.map((nu) => """
+ {
+ "name": "${nu[0]}",
+ "rootUri": "${nu[1]}"
+ }""").join(",\n")}
+ ]
+}
+""";
+
+/// Mimics a directory structure of [description] and runs [loaderTest].
+///
+/// Description is a map, each key is a file entry. If the value is a map,
+/// it's a subdirectory, otherwise it's a file and the value is the content
+/// as a string.
+void loaderTest(
+ String name,
+ Map<String, Object> description,
+ void Function(Uri root, Future<Uint8List?> Function(Uri) loader) loaderTest,
+) {
+ var root = Uri(scheme: 'test', path: '/');
+ Future<Uint8List?> loader(Uri uri) async {
+ var path = uri.path;
+ if (!uri.isScheme('test') || !path.startsWith('/')) return null;
+ var parts = path.split('/');
+ Object? value = description;
+ for (var i = 1; i < parts.length; i++) {
+ if (value is! Map<String, Object?>) return null;
+ value = value[parts[i]];
+ }
+ // ignore: unnecessary_cast
+ if (value is String) return utf8.encode(value) as Uint8List;
+ return null;
+ }
+
+ test(name, () => loaderTest(root, loader));
+}
diff --git a/pkgs/package_config/test/src/util_io.dart b/pkgs/package_config/test/src/util_io.dart
new file mode 100644
index 0000000..e032556
--- /dev/null
+++ b/pkgs/package_config/test/src/util_io.dart
@@ -0,0 +1,62 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:package_config/src/util_io.dart';
+import 'package:test/test.dart';
+
+/// Creates a directory structure from [description] and runs [fileTest].
+///
+/// Description is a map, each key is a file entry. If the value is a map,
+/// it's a subdirectory, otherwise it's a file and the value is the content
+/// as a string.
+/// Introduces a group to hold the [setUp]/[tearDown] logic.
+void fileTest(String name, Map<String, Object> description,
+ void Function(Directory directory) fileTest) {
+ group('file-test', () {
+ var tempDir = Directory.systemTemp.createTempSync('pkgcfgtest');
+ setUp(() {
+ _createFiles(tempDir, description);
+ });
+ tearDown(() {
+ tempDir.deleteSync(recursive: true);
+ });
+ test(name, () => fileTest(tempDir));
+ });
+}
+
+/// Creates a set of files under a new temporary directory.
+/// Returns the temporary directory.
+///
+/// The [description] is a map from file names to content.
+/// If the content is again a map, it represents a subdirectory
+/// with the content as description.
+/// Otherwise the content should be a string,
+/// which is written to the file as UTF-8.
+// Directory createTestFiles(Map<String, Object> description) {
+// var target = Directory.systemTemp.createTempSync("pkgcfgtest");
+// _createFiles(target, description);
+// return target;
+// }
+
+// Creates temporary files in the target directory.
+void _createFiles(Directory target, Map<Object?, Object?> description) {
+ description.forEach((name, content) {
+ var entryName = pathJoin(target.path, '$name');
+ if (content is Map<Object?, Object?>) {
+ _createFiles(Directory(entryName)..createSync(), content);
+ } else {
+ File(entryName).writeAsStringSync(content as String, flush: true);
+ }
+ });
+}
+
+/// Creates a [Directory] for a subdirectory of [parent].
+Directory subdir(Directory parent, String dirName) =>
+ Directory(pathJoinAll([parent.path, ...dirName.split('/')]));
+
+/// Creates a [File] for an entry in the [directory] directory.
+File dirFile(Directory directory, String fileName) =>
+ File(pathJoin(directory.path, fileName));
diff --git a/pkgs/pool/.gitignore b/pkgs/pool/.gitignore
new file mode 100644
index 0000000..e450c83
--- /dev/null
+++ b/pkgs/pool/.gitignore
@@ -0,0 +1,5 @@
+# Don’t commit the following directories created by pub.
+.dart_tool/
+.packages
+.pub/
+pubspec.lock
diff --git a/pkgs/pool/CHANGELOG.md b/pkgs/pool/CHANGELOG.md
new file mode 100644
index 0000000..56424fc
--- /dev/null
+++ b/pkgs/pool/CHANGELOG.md
@@ -0,0 +1,105 @@
+## 1.5.2-wip
+
+* Require Dart 3.4.
+* Move to `dart-lang/tools` monorepo.
+
+## 1.5.1
+
+* Populate the pubspec `repository` field.
+
+## 1.5.0
+
+* Stable release for null safety.
+
+## 1.5.0-nullsafety.3
+
+* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release
+ guidelines.
+
+## 1.5.0-nullsafety.2
+
+* Allow prerelease versions of the 2.12 sdk.
+
+## 1.5.0-nullsafety.1
+
+* Allow 2.10 stable and 2.11.0 dev SDK versions.
+
+## 1.5.0-nullsafety
+
+* Migrate to null safety.
+* `forEach`: Avoid `await null` if the `Stream` is not paused.
+ Improves trivial benchmark by 40%.
+
+## 1.4.0
+
+* Add `forEach` to `Pool` to support efficient async processing of an
+ `Iterable`.
+
+* Throw ArgumentError if poolSize <= 0
+
+## 1.3.6
+
+* Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 1.3.5
+
+- Updated SDK version to 2.0.0-dev.17.0
+
+## 1.3.4
+
+* Modify code to eliminate Future flattening.
+
+## 1.3.3
+
+* Declare support for `async` 2.0.0.
+
+## 1.3.2
+
+* Update to make the code work with strong-mode clean Zone API.
+
+* Required minimum SDK of 1.23.0.
+
+## 1.3.1
+
+* Fix the type annotation of `Pool.withResource()` to indicate that it takes
+ `() -> FutureOr<T>`.
+
+## 1.3.0
+
+* Add a `Pool.done` getter that returns the same future returned by
+ `Pool.close()`.
+
+## 1.2.4
+
+* Fix a strong-mode error.
+
+## 1.2.3
+
+* Fix a bug in which `Pool.withResource()` could throw a `StateError` when
+ called immediately before closing the pool.
+
+## 1.2.2
+
+* Fix strong mode warnings and add generic method annotations.
+
+## 1.2.1
+
+* Internal changes only.
+
+## 1.2.0
+
+* Add `Pool.close()`, which forbids new resource requests and releases all
+ releasable resources.
+
+## 1.1.0
+
+* Add `PoolResource.allowRelease()`, which allows a resource to indicate that it
+ can be released without forcing it to deallocate immediately.
+
+## 1.0.2
+
+* Fixed the homepage.
+
+## 1.0.1
+
+* A `TimeoutException` is now correctly thrown if the pool detects a deadlock.
diff --git a/pkgs/pool/LICENSE b/pkgs/pool/LICENSE
new file mode 100644
index 0000000..000cd7b
--- /dev/null
+++ b/pkgs/pool/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/pool/README.md b/pkgs/pool/README.md
new file mode 100644
index 0000000..461e872
--- /dev/null
+++ b/pkgs/pool/README.md
@@ -0,0 +1,57 @@
+[](https://github.com/dart-lang/tools/actions/workflows/pool.yaml)
+[](https://pub.dev/packages/pool)
+[](https://pub.dev/packages/pool/publisher)
+
+The pool package exposes a `Pool` class which makes it easy to manage a limited
+pool of resources.
+
+The easiest way to use a pool is by calling `withResource`. This runs a callback
+and returns its result, but only once there aren't too many other callbacks
+currently running.
+
+```dart
+// Create a Pool that will only allocate 10 resources at once. After 30 seconds
+// of inactivity with all resources checked out, the pool will throw an error.
+final pool = new Pool(10, timeout: new Duration(seconds: 30));
+
+Future<String> readFile(String path) {
+ // Since the call to [File.readAsString] is within [withResource], no more
+ // than ten files will be open at once.
+ return pool.withResource(() => new File(path).readAsString());
+}
+```
+
+For more fine-grained control, the user can also explicitly request generic
+`PoolResource` objects that can later be released back into the pool. This is
+what `withResource` does under the covers: requests a resource, then releases it
+once the callback completes.
+
+`Pool` ensures that only a limited number of resources are allocated at once.
+It's the caller's responsibility to ensure that the corresponding physical
+resource is only consumed when a `PoolResource` is allocated.
+
+```dart
+class PooledFile implements RandomAccessFile {
+ final RandomAccessFile _file;
+ final PoolResource _resource;
+
+ static Future<PooledFile> open(String path) {
+ return pool.request().then((resource) {
+ return new File(path).open().then((file) {
+ return new PooledFile._(file, resource);
+ });
+ });
+ }
+
+ PooledFile(this._file, this._resource);
+
+ // ...
+
+ Future<RandomAccessFile> close() {
+ return _file.close.then((_) {
+ _resource.release();
+ return this;
+ });
+ }
+}
+```
diff --git a/pkgs/pool/analysis_options.yaml b/pkgs/pool/analysis_options.yaml
new file mode 100644
index 0000000..44cda4d
--- /dev/null
+++ b/pkgs/pool/analysis_options.yaml
@@ -0,0 +1,5 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
diff --git a/pkgs/pool/benchmark/for_each_benchmark.dart b/pkgs/pool/benchmark/for_each_benchmark.dart
new file mode 100644
index 0000000..0cd2543
--- /dev/null
+++ b/pkgs/pool/benchmark/for_each_benchmark.dart
@@ -0,0 +1,55 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:pool/pool.dart';
+
+void main(List<String> args) async {
+ var poolSize = args.isEmpty ? 5 : int.parse(args.first);
+ print('Pool size: $poolSize');
+
+ final pool = Pool(poolSize);
+ final watch = Stopwatch()..start();
+ final start = DateTime.now();
+
+ DateTime? lastLog;
+ Duration? fastest;
+ late int fastestIteration;
+ var i = 1;
+
+ void log(bool force) {
+ var now = DateTime.now();
+ if (force ||
+ lastLog == null ||
+ now.difference(lastLog!) > const Duration(seconds: 1)) {
+ lastLog = now;
+ print([
+ now.difference(start),
+ i.toString().padLeft(10),
+ fastestIteration.toString().padLeft(7),
+ fastest!.inMicroseconds.toString().padLeft(9)
+ ].join(' '));
+ }
+ }
+
+ print(['Elapsed ', 'Iterations', 'Fastest', 'Time (us)'].join(' '));
+
+ for (;; i++) {
+ watch.reset();
+
+ var sum = await pool
+ .forEach<int, int>(Iterable<int>.generate(100000), (i) => i)
+ .reduce((a, b) => a + b);
+
+ assert(sum == 4999950000, 'was $sum');
+
+ var elapsed = watch.elapsed;
+ if (fastest == null || fastest > elapsed) {
+ fastest = elapsed;
+ fastestIteration = i;
+ log(true);
+ } else {
+ log(false);
+ }
+ }
+}
diff --git a/pkgs/pool/lib/pool.dart b/pkgs/pool/lib/pool.dart
new file mode 100644
index 0000000..70e9df1
--- /dev/null
+++ b/pkgs/pool/lib/pool.dart
@@ -0,0 +1,380 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:collection';
+
+import 'package:async/async.dart';
+import 'package:stack_trace/stack_trace.dart';
+
+/// Manages an abstract pool of resources with a limit on how many may be in use
+/// at once.
+///
+/// When a resource is needed, the user should call [request]. When the returned
+/// future completes with a [PoolResource], the resource may be allocated. Once
+/// the resource has been released, the user should call [PoolResource.release].
+/// The pool will ensure that only a certain number of [PoolResource]s may be
+/// allocated at once.
+class Pool {
+ /// Completers for requests beyond the first [_maxAllocatedResources].
+ ///
+ /// When an item is released, the next element of [_requestedResources] will
+ /// be completed.
+ final _requestedResources = Queue<Completer<PoolResource>>();
+
+ /// Callbacks that must be called before additional resources can be
+ /// allocated.
+ ///
+ /// See [PoolResource.allowRelease].
+ final _onReleaseCallbacks = Queue<void Function()>();
+
+ /// Completers that will be completed once `onRelease` callbacks are done
+ /// running.
+ ///
+ /// These are kept in a queue to ensure that the earliest request completes
+ /// first regardless of what order the `onRelease` callbacks complete in.
+ final _onReleaseCompleters = Queue<Completer<PoolResource>>();
+
+ /// The maximum number of resources that may be allocated at once.
+ final int _maxAllocatedResources;
+
+ /// The number of resources that are currently allocated.
+ int _allocatedResources = 0;
+
+ /// The timeout timer.
+ ///
+ /// This timer is canceled as long as the pool is below the resource limit.
+ /// It's reset once the resource limit is reached and again every time an
+ /// resource is released or a new resource is requested. If it fires, that
+ /// indicates that the caller became deadlocked, likely due to files waiting
+ /// for additional files to be read before they could be closed.
+ ///
+ /// This is `null` if this pool shouldn't time out.
+ RestartableTimer? _timer;
+
+ /// The amount of time to wait before timing out the pending resources.
+ final Duration? _timeout;
+
+ /// A [FutureGroup] that tracks all the `onRelease` callbacks for resources
+ /// that have been marked releasable.
+ ///
+ /// This is `null` until [close] is called.
+ FutureGroup? _closeGroup;
+
+ /// Whether [close] has been called.
+ bool get isClosed => _closeMemo.hasRun;
+
+ /// A future that completes once the pool is closed and all its outstanding
+ /// resources have been released.
+ ///
+ /// If any [PoolResource.allowRelease] callback throws an exception after the
+ /// pool is closed, this completes with that exception.
+ Future get done => _closeMemo.future;
+
+ /// Creates a new pool with the given limit on how many resources may be
+ /// allocated at once.
+ ///
+ /// If [timeout] is passed, then if that much time passes without any activity
+ /// all pending [request] futures will throw a [TimeoutException]. This is
+ /// intended to avoid deadlocks.
+ Pool(this._maxAllocatedResources, {Duration? timeout}) : _timeout = timeout {
+ if (_maxAllocatedResources <= 0) {
+ throw ArgumentError.value(_maxAllocatedResources, 'maxAllocatedResources',
+ 'Must be greater than zero.');
+ }
+
+ if (timeout != null) {
+ // Start the timer canceled since we only want to start counting down once
+ // we've run out of available resources.
+ _timer = RestartableTimer(timeout, _onTimeout)..cancel();
+ }
+ }
+
+ /// Request a [PoolResource].
+ ///
+ /// If the maximum number of resources is already allocated, this will delay
+ /// until one of them is released.
+ Future<PoolResource> request() {
+ if (isClosed) {
+ throw StateError('request() may not be called on a closed Pool.');
+ }
+
+ if (_allocatedResources < _maxAllocatedResources) {
+ _allocatedResources++;
+ return Future.value(PoolResource._(this));
+ } else if (_onReleaseCallbacks.isNotEmpty) {
+ return _runOnRelease(_onReleaseCallbacks.removeFirst());
+ } else {
+ var completer = Completer<PoolResource>();
+ _requestedResources.add(completer);
+ _resetTimer();
+ return completer.future;
+ }
+ }
+
+ /// Requests a resource for the duration of [callback], which may return a
+ /// Future.
+ ///
+ /// The return value of [callback] is piped to the returned Future.
+ Future<T> withResource<T>(FutureOr<T> Function() callback) async {
+ if (isClosed) {
+ throw StateError('withResource() may not be called on a closed Pool.');
+ }
+
+ var resource = await request();
+ try {
+ return await callback();
+ } finally {
+ resource.release();
+ }
+ }
+
+ /// Returns a [Stream] containing the result of [action] applied to each
+ /// element of [elements].
+ ///
+ /// While [action] is invoked on each element of [elements] in order,
+ /// it's possible the return [Stream] may have items out-of-order – especially
+ /// if the completion time of [action] varies.
+ ///
+ /// If [action] throws an error the source item along with the error object
+ /// and [StackTrace] are passed to [onError], if it is provided. If [onError]
+ /// returns `true`, the error is added to the returned [Stream], otherwise
+ /// it is ignored.
+ ///
+ /// Errors thrown from iterating [elements] will not be passed to
+ /// [onError]. They will always be added to the returned stream as an error.
+ ///
+ /// Note: all of the resources of the this [Pool] will be used when the
+ /// returned [Stream] is listened to until it is completed or canceled.
+ ///
+ /// Note: if this [Pool] is closed before the returned [Stream] is listened
+ /// to, a [StateError] is thrown.
+ Stream<T> forEach<S, T>(
+ Iterable<S> elements, FutureOr<T> Function(S source) action,
+ {bool Function(S item, Object error, StackTrace stack)? onError}) {
+ onError ??= (item, e, s) => true;
+
+ var cancelPending = false;
+
+ Completer? resumeCompleter;
+ late StreamController<T> controller;
+
+ late Iterator<S> iterator;
+
+ Future<void> run(int _) async {
+ while (iterator.moveNext()) {
+ // caching `current` is necessary because there are async breaks
+ // in this code and `iterator` is shared across many workers
+ final current = iterator.current;
+
+ _resetTimer();
+
+ if (resumeCompleter != null) {
+ await resumeCompleter!.future;
+ }
+
+ if (cancelPending) {
+ break;
+ }
+
+ T value;
+ try {
+ value = await action(current);
+ } catch (e, stack) {
+ if (onError!(current, e, stack)) {
+ controller.addError(e, stack);
+ }
+ continue;
+ }
+ controller.add(value);
+ }
+ }
+
+ Future<void>? doneFuture;
+
+ void onListen() {
+ iterator = elements.iterator;
+
+ assert(doneFuture == null);
+ var futures = Iterable<Future<void>>.generate(
+ _maxAllocatedResources, (i) => withResource(() => run(i)));
+ doneFuture = Future.wait(futures, eagerError: true)
+ .then<void>((_) {})
+ .catchError(controller.addError);
+
+ doneFuture!.whenComplete(controller.close);
+ }
+
+ controller = StreamController<T>(
+ sync: true,
+ onListen: onListen,
+ onCancel: () async {
+ assert(!cancelPending);
+ cancelPending = true;
+ await doneFuture;
+ },
+ onPause: () {
+ assert(resumeCompleter == null);
+ resumeCompleter = Completer<void>();
+ },
+ onResume: () {
+ assert(resumeCompleter != null);
+ resumeCompleter!.complete();
+ resumeCompleter = null;
+ },
+ );
+
+ return controller.stream;
+ }
+
+ /// Closes the pool so that no more resources are requested.
+ ///
+ /// Existing resource requests remain unchanged.
+ ///
+ /// Any resources that are marked as releasable using
+ /// [PoolResource.allowRelease] are released immediately. Once all resources
+ /// have been released and any `onRelease` callbacks have completed, the
+ /// returned future completes successfully. If any `onRelease` callback throws
+ /// an error, the returned future completes with that error.
+ ///
+ /// This may be called more than once; it returns the same [Future] each time.
+ Future close() => _closeMemo.runOnce(_close);
+
+ Future<void> _close() {
+ if (_closeGroup != null) return _closeGroup!.future;
+
+ _resetTimer();
+
+ _closeGroup = FutureGroup();
+ for (var callback in _onReleaseCallbacks) {
+ _closeGroup!.add(Future.sync(callback));
+ }
+
+ _allocatedResources -= _onReleaseCallbacks.length;
+ _onReleaseCallbacks.clear();
+
+ if (_allocatedResources == 0) _closeGroup!.close();
+ return _closeGroup!.future;
+ }
+
+ final _closeMemo = AsyncMemoizer<void>();
+
+ /// If there are any pending requests, this will fire the oldest one.
+ void _onResourceReleased() {
+ _resetTimer();
+
+ if (_requestedResources.isNotEmpty) {
+ var pending = _requestedResources.removeFirst();
+ pending.complete(PoolResource._(this));
+ } else {
+ _allocatedResources--;
+ if (isClosed && _allocatedResources == 0) _closeGroup!.close();
+ }
+ }
+
+ /// If there are any pending requests, this will fire the oldest one after
+ /// running [onRelease].
+ void _onResourceReleaseAllowed(void Function() onRelease) {
+ _resetTimer();
+
+ if (_requestedResources.isNotEmpty) {
+ var pending = _requestedResources.removeFirst();
+ pending.complete(_runOnRelease(onRelease));
+ } else if (isClosed) {
+ _closeGroup!.add(Future.sync(onRelease));
+ _allocatedResources--;
+ if (_allocatedResources == 0) _closeGroup!.close();
+ } else {
+ var zone = Zone.current;
+ var registered = zone.registerCallback(onRelease);
+ _onReleaseCallbacks.add(() => zone.run(registered));
+ }
+ }
+
+ /// Runs [onRelease] and returns a Future that completes to a resource once an
+ /// [onRelease] callback completes.
+ ///
+ /// Futures returned by [_runOnRelease] always complete in the order they were
+ /// created, even if earlier [onRelease] callbacks take longer to run.
+ Future<PoolResource> _runOnRelease(void Function() onRelease) {
+ Future.sync(onRelease).then((value) {
+ _onReleaseCompleters.removeFirst().complete(PoolResource._(this));
+ }).catchError((Object error, StackTrace stackTrace) {
+ _onReleaseCompleters.removeFirst().completeError(error, stackTrace);
+ });
+
+ var completer = Completer<PoolResource>.sync();
+ _onReleaseCompleters.add(completer);
+ return completer.future;
+ }
+
+ /// A resource has been requested, allocated, or released.
+ void _resetTimer() {
+ if (_timer == null) return;
+
+ if (_requestedResources.isEmpty) {
+ _timer!.cancel();
+ } else {
+ _timer!.reset();
+ }
+ }
+
+ /// Handles [_timer] timing out by causing all pending resource completers to
+ /// emit exceptions.
+ void _onTimeout() {
+ for (var completer in _requestedResources) {
+ completer.completeError(
+ TimeoutException(
+ 'Pool deadlock: all resources have been '
+ 'allocated for too long.',
+ _timeout),
+ Chain.current());
+ }
+ _requestedResources.clear();
+ _timer = null;
+ }
+}
+
+/// A member of a [Pool].
+///
+/// A [PoolResource] is a token that indicates that a resource is allocated.
+/// When the associated resource is released, the user should call [release].
+class PoolResource {
+ final Pool _pool;
+
+ /// Whether `this` has been released yet.
+ bool _released = false;
+
+ PoolResource._(this._pool);
+
+ /// Tells the parent [Pool] that the resource associated with this resource is
+ /// no longer allocated, and that a new [PoolResource] may be allocated.
+ void release() {
+ if (_released) {
+ throw StateError('A PoolResource may only be released once.');
+ }
+ _released = true;
+ _pool._onResourceReleased();
+ }
+
+ /// Tells the parent [Pool] that the resource associated with this resource is
+ /// no longer necessary, but should remain allocated until more resources are
+ /// needed.
+ ///
+ /// When [Pool.request] is called and there are no remaining available
+ /// resources, the [onRelease] callback is called. It should free the
+ /// resource, and it may return a Future or `null`. Once that completes, the
+ /// [Pool.request] call will complete to a new [PoolResource].
+ ///
+ /// This is useful when a resource's main function is complete, but it may
+ /// produce additional information later on. For example, an isolate's task
+ /// may be complete, but it could still emit asynchronous errors.
+ void allowRelease(FutureOr<void> Function() onRelease) {
+ if (_released) {
+ throw StateError('A PoolResource may only be released once.');
+ }
+ _released = true;
+ _pool._onResourceReleaseAllowed(onRelease);
+ }
+}
diff --git a/pkgs/pool/pubspec.yaml b/pkgs/pool/pubspec.yaml
new file mode 100644
index 0000000..a205b74
--- /dev/null
+++ b/pkgs/pool/pubspec.yaml
@@ -0,0 +1,18 @@
+name: pool
+version: 1.5.2-wip
+description: >-
+ Manage a finite pool of resources.
+ Useful for controlling concurrent file system or network requests.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/pool
+
+environment:
+ sdk: ^3.4.0
+
+dependencies:
+ async: ^2.5.0
+ stack_trace: ^1.10.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
+ fake_async: ^1.2.0
+ test: ^1.16.6
diff --git a/pkgs/pool/test/pool_test.dart b/pkgs/pool/test/pool_test.dart
new file mode 100644
index 0000000..6334a8a
--- /dev/null
+++ b/pkgs/pool/test/pool_test.dart
@@ -0,0 +1,745 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:fake_async/fake_async.dart';
+import 'package:pool/pool.dart';
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('request()', () {
+ test('resources can be requested freely up to the limit', () {
+ var pool = Pool(50);
+ for (var i = 0; i < 50; i++) {
+ expect(pool.request(), completes);
+ }
+ });
+
+ test('resources block past the limit', () {
+ FakeAsync().run((async) {
+ var pool = Pool(50);
+ for (var i = 0; i < 50; i++) {
+ expect(pool.request(), completes);
+ }
+ expect(pool.request(), doesNotComplete);
+
+ async.elapse(const Duration(seconds: 1));
+ });
+ });
+
+ test('a blocked resource is allocated when another is released', () {
+ FakeAsync().run((async) {
+ var pool = Pool(50);
+ for (var i = 0; i < 49; i++) {
+ expect(pool.request(), completes);
+ }
+
+ pool.request().then((lastAllocatedResource) {
+ // This will only complete once [lastAllocatedResource] is released.
+ expect(pool.request(), completes);
+
+ Future<void>.delayed(const Duration(microseconds: 1)).then((_) {
+ lastAllocatedResource.release();
+ });
+ });
+
+ async.elapse(const Duration(seconds: 1));
+ });
+ });
+ });
+
+ group('withResource()', () {
+ test('can be called freely up to the limit', () {
+ var pool = Pool(50);
+ for (var i = 0; i < 50; i++) {
+ pool.withResource(expectAsync0(() => Completer<void>().future));
+ }
+ });
+
+ test('blocks the callback past the limit', () {
+ FakeAsync().run((async) {
+ var pool = Pool(50);
+ for (var i = 0; i < 50; i++) {
+ pool.withResource(expectAsync0(() => Completer<void>().future));
+ }
+ pool.withResource(expectNoAsync());
+
+ async.elapse(const Duration(seconds: 1));
+ });
+ });
+
+ test('a blocked resource is allocated when another is released', () {
+ FakeAsync().run((async) {
+ var pool = Pool(50);
+ for (var i = 0; i < 49; i++) {
+ pool.withResource(expectAsync0(() => Completer<void>().future));
+ }
+
+ var completer = Completer<void>();
+ pool.withResource(() => completer.future);
+ var blockedResourceAllocated = false;
+ pool.withResource(() {
+ blockedResourceAllocated = true;
+ });
+
+ Future<void>.delayed(const Duration(microseconds: 1)).then((_) {
+ expect(blockedResourceAllocated, isFalse);
+ completer.complete();
+ return Future<void>.delayed(const Duration(microseconds: 1));
+ }).then((_) {
+ expect(blockedResourceAllocated, isTrue);
+ });
+
+ async.elapse(const Duration(seconds: 1));
+ });
+ });
+
+ // Regression test for #3.
+ test('can be called immediately before close()', () async {
+ var pool = Pool(1);
+ unawaited(pool.withResource(expectAsync0(() {})));
+ await pool.close();
+ });
+ });
+
+ group('with a timeout', () {
+ test("doesn't time out if there are no pending requests", () {
+ FakeAsync().run((async) {
+ var pool = Pool(50, timeout: const Duration(seconds: 5));
+ for (var i = 0; i < 50; i++) {
+ expect(pool.request(), completes);
+ }
+
+ async.elapse(const Duration(seconds: 6));
+ });
+ });
+
+ test('resets the timer if a resource is returned', () {
+ FakeAsync().run((async) {
+ var pool = Pool(50, timeout: const Duration(seconds: 5));
+ for (var i = 0; i < 49; i++) {
+ expect(pool.request(), completes);
+ }
+
+ pool.request().then((lastAllocatedResource) {
+ // This will only complete once [lastAllocatedResource] is released.
+ expect(pool.request(), completes);
+
+ Future<void>.delayed(const Duration(seconds: 3)).then((_) {
+ lastAllocatedResource.release();
+ expect(pool.request(), doesNotComplete);
+ });
+ });
+
+ async.elapse(const Duration(seconds: 6));
+ });
+ });
+
+ test('resets the timer if a resource is requested', () {
+ FakeAsync().run((async) {
+ var pool = Pool(50, timeout: const Duration(seconds: 5));
+ for (var i = 0; i < 50; i++) {
+ expect(pool.request(), completes);
+ }
+ expect(pool.request(), doesNotComplete);
+
+ Future<void>.delayed(const Duration(seconds: 3)).then((_) {
+ expect(pool.request(), doesNotComplete);
+ });
+
+ async.elapse(const Duration(seconds: 6));
+ });
+ });
+
+ test('times out if nothing happens', () {
+ FakeAsync().run((async) {
+ var pool = Pool(50, timeout: const Duration(seconds: 5));
+ for (var i = 0; i < 50; i++) {
+ expect(pool.request(), completes);
+ }
+ expect(pool.request(), throwsA(const TypeMatcher<TimeoutException>()));
+
+ async.elapse(const Duration(seconds: 6));
+ });
+ });
+ });
+
+ group('allowRelease()', () {
+ test('runs the callback once the resource limit is exceeded', () async {
+ var pool = Pool(50);
+ for (var i = 0; i < 49; i++) {
+ expect(pool.request(), completes);
+ }
+
+ var resource = await pool.request();
+ var onReleaseCalled = false;
+ resource.allowRelease(() => onReleaseCalled = true);
+ await Future<void>.delayed(Duration.zero);
+ expect(onReleaseCalled, isFalse);
+
+ expect(pool.request(), completes);
+ await Future<void>.delayed(Duration.zero);
+ expect(onReleaseCalled, isTrue);
+ });
+
+ test('runs the callback immediately if there are blocked requests',
+ () async {
+ var pool = Pool(1);
+ var resource = await pool.request();
+
+ // This will be blocked until [resource.allowRelease] is called.
+ expect(pool.request(), completes);
+
+ var onReleaseCalled = false;
+ resource.allowRelease(() => onReleaseCalled = true);
+ await Future<void>.delayed(Duration.zero);
+ expect(onReleaseCalled, isTrue);
+ });
+
+ test('blocks the request until the callback completes', () async {
+ var pool = Pool(1);
+ var resource = await pool.request();
+
+ var requestComplete = false;
+ unawaited(pool.request().then((_) => requestComplete = true));
+
+ var completer = Completer<void>();
+ resource.allowRelease(() => completer.future);
+ await Future<void>.delayed(Duration.zero);
+ expect(requestComplete, isFalse);
+
+ completer.complete();
+ await Future<void>.delayed(Duration.zero);
+ expect(requestComplete, isTrue);
+ });
+
+ test('completes requests in request order regardless of callback order',
+ () async {
+ var pool = Pool(2);
+ var resource1 = await pool.request();
+ var resource2 = await pool.request();
+
+ var request1Complete = false;
+ unawaited(pool.request().then((_) => request1Complete = true));
+ var request2Complete = false;
+ unawaited(pool.request().then((_) => request2Complete = true));
+
+ var onRelease1Called = false;
+ var completer1 = Completer<void>();
+ resource1.allowRelease(() {
+ onRelease1Called = true;
+ return completer1.future;
+ });
+ await Future<void>.delayed(Duration.zero);
+ expect(onRelease1Called, isTrue);
+
+ var onRelease2Called = false;
+ var completer2 = Completer<void>();
+ resource2.allowRelease(() {
+ onRelease2Called = true;
+ return completer2.future;
+ });
+ await Future<void>.delayed(Duration.zero);
+ expect(onRelease2Called, isTrue);
+ expect(request1Complete, isFalse);
+ expect(request2Complete, isFalse);
+
+ // Complete the second resource's onRelease callback first. Even though it
+ // was triggered by the second blocking request, it should complete the
+ // first one to preserve ordering.
+ completer2.complete();
+ await Future<void>.delayed(Duration.zero);
+ expect(request1Complete, isTrue);
+ expect(request2Complete, isFalse);
+
+ completer1.complete();
+ await Future<void>.delayed(Duration.zero);
+ expect(request1Complete, isTrue);
+ expect(request2Complete, isTrue);
+ });
+
+ test('runs onRequest in the zone it was created', () async {
+ var pool = Pool(1);
+ var resource = await pool.request();
+
+ var outerZone = Zone.current;
+ runZoned(() {
+ var innerZone = Zone.current;
+ expect(innerZone, isNot(equals(outerZone)));
+
+ resource.allowRelease(expectAsync0(() {
+ expect(Zone.current, equals(innerZone));
+ }));
+ });
+
+ await pool.request();
+ });
+ });
+
+ test("done doesn't complete without close", () async {
+ var pool = Pool(1);
+ unawaited(pool.done.then(expectAsync1((_) {}, count: 0)));
+
+ var resource = await pool.request();
+ resource.release();
+
+ await Future<void>.delayed(Duration.zero);
+ });
+
+ group('close()', () {
+ test('disallows request() and withResource()', () {
+ var pool = Pool(1)..close();
+ expect(pool.request, throwsStateError);
+ expect(() => pool.withResource(() {}), throwsStateError);
+ });
+
+ test('pending requests are fulfilled', () async {
+ var pool = Pool(1);
+ var resource1 = await pool.request();
+ expect(
+ pool.request().then((resource2) {
+ resource2.release();
+ }),
+ completes);
+ expect(pool.done, completes);
+ expect(pool.close(), completes);
+ resource1.release();
+ });
+
+ test('pending requests are fulfilled with allowRelease', () async {
+ var pool = Pool(1);
+ var resource1 = await pool.request();
+
+ var completer = Completer<void>();
+ expect(
+ pool.request().then((resource2) {
+ expect(completer.isCompleted, isTrue);
+ resource2.release();
+ }),
+ completes);
+ expect(pool.close(), completes);
+
+ resource1.allowRelease(() => completer.future);
+ await Future<void>.delayed(Duration.zero);
+
+ completer.complete();
+ });
+
+ test("doesn't complete until all resources are released", () async {
+ var pool = Pool(2);
+ var resource1 = await pool.request();
+ var resource2 = await pool.request();
+ var resource3Future = pool.request();
+
+ var resource1Released = false;
+ var resource2Released = false;
+ var resource3Released = false;
+ expect(
+ pool.close().then((_) {
+ expect(resource1Released, isTrue);
+ expect(resource2Released, isTrue);
+ expect(resource3Released, isTrue);
+ }),
+ completes);
+
+ resource1Released = true;
+ resource1.release();
+ await Future<void>.delayed(Duration.zero);
+
+ resource2Released = true;
+ resource2.release();
+ await Future<void>.delayed(Duration.zero);
+
+ var resource3 = await resource3Future;
+ resource3Released = true;
+ resource3.release();
+ });
+
+ test('active onReleases complete as usual', () async {
+ var pool = Pool(1);
+ var resource = await pool.request();
+
+ // Set up an onRelease callback whose completion is controlled by
+ // [completer].
+ var completer = Completer<void>();
+ resource.allowRelease(() => completer.future);
+ expect(
+ pool.request().then((_) {
+ expect(completer.isCompleted, isTrue);
+ }),
+ completes);
+
+ await Future<void>.delayed(Duration.zero);
+ unawaited(pool.close());
+
+ await Future<void>.delayed(Duration.zero);
+ completer.complete();
+ });
+
+ test('inactive onReleases fire', () async {
+ var pool = Pool(2);
+ var resource1 = await pool.request();
+ var resource2 = await pool.request();
+
+ var completer1 = Completer<void>();
+ resource1.allowRelease(() => completer1.future);
+ var completer2 = Completer<void>();
+ resource2.allowRelease(() => completer2.future);
+
+ expect(
+ pool.close().then((_) {
+ expect(completer1.isCompleted, isTrue);
+ expect(completer2.isCompleted, isTrue);
+ }),
+ completes);
+
+ await Future<void>.delayed(Duration.zero);
+ completer1.complete();
+
+ await Future<void>.delayed(Duration.zero);
+ completer2.complete();
+ });
+
+ test('new allowReleases fire immediately', () async {
+ var pool = Pool(1);
+ var resource = await pool.request();
+
+ var completer = Completer<void>();
+ expect(
+ pool.close().then((_) {
+ expect(completer.isCompleted, isTrue);
+ }),
+ completes);
+
+ await Future<void>.delayed(Duration.zero);
+ resource.allowRelease(() => completer.future);
+
+ await Future<void>.delayed(Duration.zero);
+ completer.complete();
+ });
+
+ test('an onRelease error is piped to the return value', () async {
+ var pool = Pool(1);
+ var resource = await pool.request();
+
+ var completer = Completer<void>();
+ resource.allowRelease(() => completer.future);
+
+ expect(pool.done, throwsA('oh no!'));
+ expect(pool.close(), throwsA('oh no!'));
+
+ await Future<void>.delayed(Duration.zero);
+ completer.completeError('oh no!');
+ });
+ });
+
+ group('forEach', () {
+ late Pool pool;
+
+ tearDown(() async {
+ await pool.close();
+ });
+
+ const delayedToStringDuration = Duration(milliseconds: 10);
+
+ Future<String> delayedToString(int i) =>
+ Future<String>.delayed(delayedToStringDuration, () => i.toString());
+
+ for (var itemCount in [0, 5]) {
+ for (var poolSize in [1, 5, 6]) {
+ test('poolSize: $poolSize, itemCount: $itemCount', () async {
+ pool = Pool(poolSize);
+
+ var finishedItems = 0;
+
+ await for (var item in pool.forEach(
+ Iterable.generate(itemCount, (i) {
+ expect(i, lessThanOrEqualTo(finishedItems + poolSize),
+ reason: 'the iterator should be called lazily');
+ return i;
+ }),
+ delayedToString)) {
+ expect(int.parse(item), lessThan(itemCount));
+ finishedItems++;
+ }
+
+ expect(finishedItems, itemCount);
+ });
+ }
+ }
+
+ test('pool closed before listen', () async {
+ pool = Pool(2);
+
+ var stream = pool.forEach(Iterable<int>.generate(5), delayedToString);
+
+ await pool.close();
+
+ expect(stream.toList(), throwsStateError);
+ });
+
+ test('completes even if the pool is partially used', () async {
+ pool = Pool(2);
+
+ var resource = await pool.request();
+
+ var stream = pool.forEach(<int>[], delayedToString);
+
+ expect(await stream.length, 0);
+
+ resource.release();
+ });
+
+ test('stream paused longer than timeout', () async {
+ pool = Pool(2, timeout: delayedToStringDuration);
+
+ var resource = await pool.request();
+
+ var stream = pool.forEach<int, int>(
+ Iterable.generate(100, (i) {
+ expect(i, lessThan(20),
+ reason: 'The timeout should happen '
+ 'before the entire iterable is iterated.');
+ return i;
+ }), (i) async {
+ await Future<void>.delayed(Duration(milliseconds: i));
+ return i;
+ });
+
+ await expectLater(
+ stream.toList,
+ throwsA(const TypeMatcher<TimeoutException>().having(
+ (te) => te.message,
+ 'message',
+ contains('Pool deadlock: '
+ 'all resources have been allocated for too long.'))));
+
+ resource.release();
+ });
+
+ group('timing and timeout', () {
+ for (var poolSize in [2, 8, 64]) {
+ for (var otherTaskCount
+ in [0, 1, 7, 63].where((otc) => otc < poolSize)) {
+ test('poolSize: $poolSize, otherTaskCount: $otherTaskCount',
+ () async {
+ final itemCount = 128;
+ pool = Pool(poolSize, timeout: const Duration(milliseconds: 20));
+
+ var otherTasks = await Future.wait(
+ Iterable<int>.generate(otherTaskCount)
+ .map((i) => pool.request()));
+
+ try {
+ var finishedItems = 0;
+
+ var watch = Stopwatch()..start();
+
+ await for (var item in pool.forEach(
+ Iterable.generate(itemCount, (i) {
+ expect(i, lessThanOrEqualTo(finishedItems + poolSize),
+ reason: 'the iterator should be called lazily');
+ return i;
+ }),
+ delayedToString)) {
+ expect(int.parse(item), lessThan(itemCount));
+ finishedItems++;
+ }
+
+ expect(finishedItems, itemCount);
+
+ final expectedElapsed =
+ delayedToStringDuration.inMicroseconds * 4;
+
+ expect((watch.elapsed ~/ itemCount).inMicroseconds,
+ lessThan(expectedElapsed / (poolSize - otherTaskCount)),
+ reason: 'Average time per task should be '
+ 'proportionate to the available pool resources.');
+ } finally {
+ for (var task in otherTasks) {
+ task.release();
+ }
+ }
+ });
+ }
+ }
+ }, testOn: 'vm');
+
+ test('partial iteration', () async {
+ pool = Pool(5);
+ var stream = pool.forEach(Iterable<int>.generate(100), delayedToString);
+ expect(await stream.take(10).toList(), hasLength(10));
+ });
+
+ test('pool close during data with waiting to be done', () async {
+ pool = Pool(5);
+
+ var stream = pool.forEach(Iterable<int>.generate(100), delayedToString);
+
+ var dataCount = 0;
+ var subscription = stream.listen((data) {
+ dataCount++;
+ pool.close();
+ });
+
+ await subscription.asFuture<void>();
+ expect(dataCount, 100);
+ await subscription.cancel();
+ });
+
+ test('pause and resume ', () async {
+ var generatedCount = 0;
+ var dataCount = 0;
+ final poolSize = 5;
+
+ pool = Pool(poolSize);
+
+ var stream = pool.forEach(
+ Iterable<int>.generate(40, (i) {
+ expect(generatedCount, lessThanOrEqualTo(dataCount + 2 * poolSize),
+ reason: 'The iterator should not be called '
+ 'much faster than the data is consumed.');
+ generatedCount++;
+ return i;
+ }),
+ delayedToString);
+
+ // ignore: cancel_subscriptions
+ late StreamSubscription subscription;
+
+ subscription = stream.listen(
+ (data) {
+ dataCount++;
+
+ if (int.parse(data) % 3 == 1) {
+ subscription.pause(Future(() async {
+ await Future<void>.delayed(const Duration(milliseconds: 100));
+ }));
+ }
+ },
+ onError: registerException,
+ onDone: expectAsync0(() {
+ expect(dataCount, 40);
+ }),
+ );
+ });
+
+ group('cancel', () {
+ final dataSize = 32;
+ for (var i = 1; i < 5; i++) {
+ test('with pool size $i', () async {
+ pool = Pool(i);
+
+ var stream =
+ pool.forEach(Iterable<int>.generate(dataSize), delayedToString);
+
+ var cancelCompleter = Completer<void>();
+
+ StreamSubscription subscription;
+
+ var eventCount = 0;
+ subscription = stream.listen((data) {
+ eventCount++;
+ if (int.parse(data) == dataSize ~/ 2) {
+ cancelCompleter.complete();
+ }
+ }, onError: registerException);
+
+ await cancelCompleter.future;
+
+ await subscription.cancel();
+
+ expect(eventCount, 1 + dataSize ~/ 2);
+ });
+ }
+ });
+
+ group('errors', () {
+ Future<void> errorInIterator({
+ bool Function(int item, Object error, StackTrace stack)? onError,
+ }) async {
+ pool = Pool(20);
+
+ var listFuture = pool
+ .forEach(
+ Iterable.generate(100, (i) {
+ if (i == 50) {
+ throw StateError('error while generating item in iterator');
+ }
+
+ return i;
+ }),
+ delayedToString,
+ onError: onError)
+ .toList();
+
+ await expectLater(() async => listFuture, throwsStateError);
+ }
+
+ test('iteration, no onError', () async {
+ await errorInIterator();
+ });
+ test('iteration, with onError', () async {
+ await errorInIterator(onError: (i, e, s) => false);
+ });
+
+ test('error in action, no onError', () async {
+ pool = Pool(20);
+
+ var listFuture = pool.forEach(Iterable<int>.generate(100), (i) async {
+ await Future<void>.delayed(const Duration(milliseconds: 10));
+ if (i == 10) {
+ throw UnsupportedError('10 is not supported');
+ }
+ return i.toString();
+ }).toList();
+
+ await expectLater(() async => listFuture, throwsUnsupportedError);
+ });
+
+ test('error in action, no onError', () async {
+ pool = Pool(20);
+
+ var list = await pool.forEach(Iterable<int>.generate(100),
+ (int i) async {
+ await Future<void>.delayed(const Duration(milliseconds: 10));
+ if (i % 10 == 0) {
+ throw UnsupportedError('Multiples of 10 not supported');
+ }
+ return i.toString();
+ },
+ onError: (item, error, stack) =>
+ error is! UnsupportedError).toList();
+
+ expect(list, hasLength(90));
+ });
+ });
+ });
+
+ test('throw error when pool limit <= 0', () {
+ expect(() => Pool(-1), throwsArgumentError);
+ expect(() => Pool(0), throwsArgumentError);
+ });
+}
+
+/// Returns a function that will cause the test to fail if it's called.
+///
+/// This should only be called within a [FakeAsync.run] zone.
+void Function() expectNoAsync() {
+ var stack = Trace.current(1);
+ return () => registerException(
+ TestFailure('Expected function not to be called.'), stack);
+}
+
+/// A matcher for Futures that asserts that they don't complete.
+///
+/// This should only be called within a [FakeAsync.run] zone.
+Matcher get doesNotComplete => predicate((Future future) {
+ var stack = Trace.current(1);
+ future.then((_) => registerException(
+ TestFailure('Expected future not to complete.'), stack));
+ return true;
+ });
diff --git a/pkgs/pub_semver/.gitignore b/pkgs/pub_semver/.gitignore
new file mode 100644
index 0000000..49ce72d
--- /dev/null
+++ b/pkgs/pub_semver/.gitignore
@@ -0,0 +1,3 @@
+.dart_tool/
+.packages
+pubspec.lock
diff --git a/pkgs/pub_semver/CHANGELOG.md b/pkgs/pub_semver/CHANGELOG.md
new file mode 100644
index 0000000..a31fbb2
--- /dev/null
+++ b/pkgs/pub_semver/CHANGELOG.md
@@ -0,0 +1,177 @@
+## 2.1.5
+
+- Require Dart `3.4.0`.
+- Move to `dart-lang/tools` monorepo.
+
+## 2.1.4
+
+- Added topics to `pubspec.yaml`.
+
+## 2.1.3
+
+- Add type parameters to the signatures of the `Version.preRelease` and
+ `Version.build` fields (`List` ==> `List<Object>`).
+ [#74](https://github.com/dart-lang/pub_semver/pull/74).
+- Require Dart 2.17.
+
+## 2.1.2
+
+- Add markdown badges to the readme.
+
+## 2.1.1
+
+- Fixed the version parsing pattern to only accept dots between version
+ components.
+
+## 2.1.0
+
+- Added `Version.canonicalizedVersion` to help scrub leading zeros and highlight
+ that `Version.toString()` preserves leading zeros.
+- Annotated `Version` with `@sealed` to discourage users from implementing the
+ interface.
+
+## 2.0.0
+
+- Stable null safety release.
+- `Version.primary` now throws `StateError` if the `versions` argument is empty.
+
+## 1.4.4
+
+- Fix a bug of `VersionRange.union` where ranges bounded at infinity would get
+ combined wrongly.
+
+# 1.4.3
+
+- Update Dart SDK constraint to `>=2.0.0 <3.0.0`.
+- Update `package:collection` constraint to `^1.0.0`.
+
+## 1.4.2
+
+* Set max SDK version to `<3.0.0`.
+
+## 1.4.1
+
+* Fix a bug where there upper bound of a version range with a build identifier
+ could accidentally be rewritten.
+
+## 1.4.0
+
+* Add a `Version.firstPreRelease` getter that returns the first possible
+ pre-release of a version.
+
+* Add a `Version.isFirstPreRelease` getter that returns whether a version is the
+ first possible pre-release.
+
+* `new VersionRange()` with an exclusive maximum now replaces the maximum with
+ its first pre-release version. This matches the existing semantics, where an
+ exclusive maximum would exclude pre-release versions of that maximum.
+
+ Explicitly representing this by changing the maximum version ensures that all
+ operations behave correctly with respect to the special pre-release semantics.
+ In particular, it fixes bugs where, for example,
+ `(>=1.0.0 <2.0.0-dev).union(>=2.0.0-dev <2.0.0)` and
+ `(>=1.0.0 <3.0.0).difference(^1.0.0)` wouldn't include `2.0.0-dev`.
+
+* Add an `alwaysIncludeMaxPreRelease` parameter to `new VersionRange()`, which
+ disables the replacement described above and allows users to create ranges
+ that do include the pre-release versions of an exclusive max version.
+
+## 1.3.7
+
+* Fix more bugs with `VersionRange.intersect()`, `VersionRange.difference()`,
+ and `VersionRange.union()` involving version ranges with pre-release maximums.
+
+## 1.3.6
+
+* Fix a bug where constraints that only allowed pre-release versions would be
+ parsed as empty constraints.
+
+## 1.3.5
+
+* Fix a bug where `VersionRange.intersect()` would return incorrect results for
+ pre-release versions with the same base version number as release versions.
+
+## 1.3.4
+
+* Fix a bug where `VersionRange.allowsAll()`, `VersionRange.allowsAny()`, and
+ `VersionRange.difference()` would return incorrect results for pre-release
+ versions with the same base version number as release versions.
+
+## 1.3.3
+
+* Fix a bug where `VersionRange.difference()` with a union constraint that
+ covered the entire range would crash.
+
+## 1.3.2
+
+* Fix a checked-mode error in `VersionRange.difference()`.
+
+## 1.3.1
+
+* Fix a new strong mode error.
+
+## 1.3.0
+
+* Make the `VersionUnion` class public. This was previously used internally to
+ implement `new VersionConstraint.unionOf()` and `VersionConstraint.union()`.
+ Now it's public so you can use it too.
+
+* Added `VersionConstraint.difference()`. This returns a constraint matching all
+ versions matched by one constraint but not another.
+
+* Make `VersionRange` implement `Comparable<VersionRange>`. Ranges are ordered
+ first by lower bound, then by upper bound.
+
+## 1.2.4
+
+* Fix all remaining strong mode warnings.
+
+## 1.2.3
+
+* Addressed three strong mode warnings.
+
+## 1.2.2
+
+* Make the package analyze under strong mode and compile with the DDC (Dart Dev
+ Compiler). Fix two issues with a private subclass of `VersionConstraint`
+ having different types for overridden methods.
+
+## 1.2.1
+
+* Allow version ranges like `>=1.2.3-dev.1 <1.2.3` to match pre-release versions
+ of `1.2.3`. Previously, these didn't match, since the pre-release versions had
+ the same major, minor, and patch numbers as the max; now an exception has been
+ added if they also have the same major, minor, and patch numbers as the min
+ *and* the min is also a pre-release version.
+
+## 1.2.0
+
+* Add a `VersionConstraint.union()` method and a `new
+ VersionConstraint.unionOf()` constructor. These each return a constraint that
+ matches multiple existing constraints.
+
+* Add a `VersionConstraint.allowsAll()` method, which returns whether one
+ constraint is a superset of another.
+
+* Add a `VersionConstraint.allowsAny()` method, which returns whether one
+ constraint overlaps another.
+
+* `Version` now implements `VersionRange`.
+
+## 1.1.0
+
+* Add support for the `^` operator for compatible versions according to pub's
+ notion of compatibility. `^1.2.3` is equivalent to `>=1.2.3 <2.0.0`; `^0.1.2`
+ is equivalent to `>=0.1.2 <0.2.0`.
+
+* Add `Version.nextBreaking`, which returns the next version that introduces
+ breaking changes after a given version.
+
+* Add `new VersionConstraint.compatibleWith()`, which returns a range covering
+ all versions compatible with a given version.
+
+* Add a custom `VersionRange.hashCode` to make it properly hashable.
+
+## 1.0.0
+
+* Initial release.
diff --git a/pkgs/pub_semver/LICENSE b/pkgs/pub_semver/LICENSE
new file mode 100644
index 0000000..000cd7b
--- /dev/null
+++ b/pkgs/pub_semver/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/pub_semver/README.md b/pkgs/pub_semver/README.md
new file mode 100644
index 0000000..03c92a3
--- /dev/null
+++ b/pkgs/pub_semver/README.md
@@ -0,0 +1,107 @@
+[](https://github.com/dart-lang/tools/actions/workflows/pub_semver.yaml)
+[](https://pub.dev/packages/pub_semver)
+[](https://pub.dev/packages/pub_semver/publisher)
+
+Handles version numbers and version constraints in the same way that [pub][]
+does.
+
+## Semantics
+
+The semantics here very closely follow the
+[Semantic Versioning spec version 2.0.0-rc.1][semver]. It differs from semver
+in a few corner cases:
+
+ * **Version ordering does take build suffixes into account.** This is unlike
+ semver 2.0.0 but like earlier versions of semver. Version `1.2.3+1` is
+ considered a lower number than `1.2.3+2`.
+
+ Since a package may have published multiple versions that differ only by
+ build suffix, pub still has to pick one of them *somehow*. Semver leaves
+ that issue unresolved, so we just say that build numbers are sorted like
+ pre-release suffixes.
+
+ * **Pre-release versions are excluded from most max ranges.** Let's say a
+ user is depending on "foo" with constraint `>=1.0.0 <2.0.0` and that "foo"
+ has published these versions:
+
+ * `1.0.0`
+ * `1.1.0`
+ * `1.2.0`
+ * `2.0.0-alpha`
+ * `2.0.0-beta`
+ * `2.0.0`
+ * `2.1.0`
+
+ Versions `2.0.0` and `2.1.0` are excluded by the constraint since neither
+ matches `<2.0.0`. However, since semver specifies that pre-release versions
+ are lower than the non-prerelease version (i.e. `2.0.0-beta < 2.0.0`, then
+ the `<2.0.0` constraint does technically allow those.
+
+ But that's almost never what the user wants. If their package doesn't work
+ with foo `2.0.0`, it's certainly not likely to work with experimental,
+ unstable versions of `2.0.0`'s API, which is what pre-release versions
+ represent.
+
+ To handle that, `<` version ranges don't allow pre-release versions of the
+ maximum unless the max is itself a pre-release, or the min is a pre-release
+ of the same version. In other words, a `<2.0.0` constraint will prohibit not
+ just `2.0.0` but any pre-release of `2.0.0`. However, `<2.0.0-beta` will
+ exclude `2.0.0-beta` but allow `2.0.0-alpha`. Likewise, `>2.0.0-alpha
+ <2.0.0` will exclude `2.0.0-alpha` but allow `2.0.0-beta`.
+
+ * **Pre-release versions are avoided when possible.** The above case
+ handles pre-release versions at the top of the range, but what about in
+ the middle? What if "foo" has these versions:
+
+ * `1.0.0`
+ * `1.2.0-alpha`
+ * `1.2.0`
+ * `1.3.0-experimental`
+
+ When a number of versions are valid, pub chooses the best one where "best"
+ usually means "highest numbered". That follows the user's intuition that,
+ all else being equal, they want the latest and greatest. Here, that would
+ mean `1.3.0-experimental`. However, most users don't want to use unstable
+ versions of their dependencies.
+
+ We want pre-releases to be explicitly opt-in so that package consumers
+ don't get unpleasant surprises and so that package maintainers are free to
+ put out pre-releases and get feedback without dragging all of their users
+ onto the bleeding edge.
+
+ To accommodate that, when pub is choosing a version, it uses *priority*
+ order which is different from strict comparison ordering. Any stable
+ version is considered higher priority than any unstable version. The above
+ versions, in priority order, are:
+
+ * `1.2.0-alpha`
+ * `1.3.0-experimental`
+ * `1.0.0`
+ * `1.2.0`
+
+ This ensures that users only end up with an unstable version when there are
+ no alternatives. Usually this means they've picked a constraint that
+ specifically selects that unstable version -- they've deliberately opted
+ into it.
+
+ * **There is a notion of compatibility between pre-1.0.0 versions.** Semver
+ deems all pre-1.0.0 versions to be incompatible. This means that the only
+ way to ensure compatibility when depending on a pre-1.0.0 package is to
+ pin the dependency to an exact version. Pinned version constraints prevent
+ automatic patch and pre-release updates. To avoid this situation, pub
+ defines the "next breaking" version as the version which increments the
+ major version if it's greater than zero, and the minor version otherwise,
+ resets subsequent digits to zero, and strips any pre-release or build
+ suffix. For example, here are some versions along with their next breaking
+ ones:
+
+ `0.0.3` -> `0.1.0`
+ `0.7.2-alpha` -> `0.8.0`
+ `1.2.3` -> `2.0.0`
+
+ To make use of this, pub defines a "^" operator which yields a version
+ constraint greater than or equal to a given version, but less than its next
+ breaking one.
+
+[pub]: https://pub.dev
+[semver]: https://semver.org/spec/v2.0.0-rc.1.html
diff --git a/pkgs/pub_semver/analysis_options.yaml b/pkgs/pub_semver/analysis_options.yaml
new file mode 100644
index 0000000..76380a0
--- /dev/null
+++ b/pkgs/pub_semver/analysis_options.yaml
@@ -0,0 +1,31 @@
+# https://dart.dev/guides/language/analysis-options
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
+ strict-inference: true
+ strict-raw-types: true
+
+linter:
+ rules:
+ - avoid_bool_literals_in_conditional_expressions
+ - avoid_classes_with_only_static_members
+ - avoid_private_typedef_functions
+ - avoid_redundant_argument_values
+ - avoid_returning_this
+ - avoid_unused_constructor_parameters
+ - avoid_void_async
+ - cancel_subscriptions
+ - cascade_invocations
+ - join_return_with_assignment
+ - literal_only_boolean_expressions
+ - missing_whitespace_between_adjacent_strings
+ - no_adjacent_strings_in_list
+ - no_runtimeType_toString
+ - prefer_const_declarations
+ - prefer_expression_function_bodies
+ - unnecessary_await_in_return
+ - use_if_null_to_convert_nulls_to_bools
+ - use_raw_strings
+ - use_string_buffers
diff --git a/pkgs/pub_semver/example/example.dart b/pkgs/pub_semver/example/example.dart
new file mode 100644
index 0000000..890343c
--- /dev/null
+++ b/pkgs/pub_semver/example/example.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:pub_semver/pub_semver.dart';
+
+void main() {
+ final range = VersionConstraint.parse('^2.0.0');
+
+ for (var version in [
+ Version.parse('1.2.3-pre'),
+ Version.parse('2.0.0+123'),
+ Version.parse('3.0.0-dev'),
+ ]) {
+ print('$version ${version.isPreRelease} ${range.allows(version)}');
+ }
+}
diff --git a/pkgs/pub_semver/lib/pub_semver.dart b/pkgs/pub_semver/lib/pub_semver.dart
new file mode 100644
index 0000000..4b6487c
--- /dev/null
+++ b/pkgs/pub_semver/lib/pub_semver.dart
@@ -0,0 +1,8 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'src/version.dart';
+export 'src/version_constraint.dart';
+export 'src/version_range.dart' hide CompatibleWithVersionRange;
+export 'src/version_union.dart';
diff --git a/pkgs/pub_semver/lib/src/patterns.dart b/pkgs/pub_semver/lib/src/patterns.dart
new file mode 100644
index 0000000..03119ac
--- /dev/null
+++ b/pkgs/pub_semver/lib/src/patterns.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Regex that matches a version number at the beginning of a string.
+final startVersion = RegExp(r'^' // Start at beginning.
+ r'(\d+)\.(\d+)\.(\d+)' // Version number.
+ r'(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Pre-release.
+ r'(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?'); // Build.
+
+/// Like [startVersion] but matches the entire string.
+final completeVersion = RegExp('${startVersion.pattern}\$');
+
+/// Parses a comparison operator ("<", ">", "<=", or ">=") at the beginning of
+/// a string.
+final startComparison = RegExp(r'^[<>]=?');
+
+/// The "compatible with" operator.
+const compatibleWithChar = '^';
diff --git a/pkgs/pub_semver/lib/src/utils.dart b/pkgs/pub_semver/lib/src/utils.dart
new file mode 100644
index 0000000..a9f714f
--- /dev/null
+++ b/pkgs/pub_semver/lib/src/utils.dart
@@ -0,0 +1,58 @@
+// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'version.dart';
+import 'version_range.dart';
+
+/// Returns whether [range1] is immediately next to, but not overlapping,
+/// [range2].
+bool areAdjacent(VersionRange range1, VersionRange range2) {
+ if (range1.max != range2.min) return false;
+
+ return (range1.includeMax && !range2.includeMin) ||
+ (!range1.includeMax && range2.includeMin);
+}
+
+/// Returns whether [range1] allows lower versions than [range2].
+bool allowsLower(VersionRange range1, VersionRange range2) {
+ if (range1.min == null) return range2.min != null;
+ if (range2.min == null) return false;
+
+ var comparison = range1.min!.compareTo(range2.min!);
+ if (comparison == -1) return true;
+ if (comparison == 1) return false;
+ return range1.includeMin && !range2.includeMin;
+}
+
+/// Returns whether [range1] allows higher versions than [range2].
+bool allowsHigher(VersionRange range1, VersionRange range2) {
+ if (range1.max == null) return range2.max != null;
+ if (range2.max == null) return false;
+
+ var comparison = range1.max!.compareTo(range2.max!);
+ if (comparison == 1) return true;
+ if (comparison == -1) return false;
+ return range1.includeMax && !range2.includeMax;
+}
+
+/// Returns whether [range1] allows only versions lower than those allowed by
+/// [range2].
+bool strictlyLower(VersionRange range1, VersionRange range2) {
+ if (range1.max == null || range2.min == null) return false;
+
+ var comparison = range1.max!.compareTo(range2.min!);
+ if (comparison == -1) return true;
+ if (comparison == 1) return false;
+ return !range1.includeMax || !range2.includeMin;
+}
+
+/// Returns whether [range1] allows only versions higher than those allowed by
+/// [range2].
+bool strictlyHigher(VersionRange range1, VersionRange range2) =>
+ strictlyLower(range2, range1);
+
+bool equalsWithoutPreRelease(Version version1, Version version2) =>
+ version1.major == version2.major &&
+ version1.minor == version2.minor &&
+ version1.patch == version2.patch;
diff --git a/pkgs/pub_semver/lib/src/version.dart b/pkgs/pub_semver/lib/src/version.dart
new file mode 100644
index 0000000..90f3d53
--- /dev/null
+++ b/pkgs/pub_semver/lib/src/version.dart
@@ -0,0 +1,391 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:math' as math;
+
+import 'package:collection/collection.dart';
+import 'package:meta/meta.dart' show sealed;
+
+import 'patterns.dart';
+import 'version_constraint.dart';
+import 'version_range.dart';
+
+/// The equality operator to use for comparing version components.
+const _equality = IterableEquality<Object>();
+
+/// A parsed semantic version number.
+@sealed
+class Version implements VersionConstraint, VersionRange {
+ /// No released version: i.e. "0.0.0".
+ static Version get none => Version(0, 0, 0);
+
+ /// Compares [a] and [b] to see which takes priority over the other.
+ ///
+ /// Returns `1` if [a] takes priority over [b] and `-1` if vice versa. If
+ /// [a] and [b] are equivalent, returns `0`.
+ ///
+ /// Unlike [compareTo], which *orders* versions, this determines which
+ /// version a user is likely to prefer. In particular, it prioritizes
+ /// pre-release versions lower than stable versions, regardless of their
+ /// version numbers. Pub uses this when determining which version to prefer
+ /// when a number of versions are allowed. In that case, it will always
+ /// choose a stable version when possible.
+ ///
+ /// When used to sort a list, orders in ascending priority so that the
+ /// highest priority version is *last* in the result.
+ static int prioritize(Version a, Version b) {
+ // Sort all prerelease versions after all normal versions. This way
+ // the solver will prefer stable packages over unstable ones.
+ if (a.isPreRelease && !b.isPreRelease) return -1;
+ if (!a.isPreRelease && b.isPreRelease) return 1;
+
+ return a.compareTo(b);
+ }
+
+ /// Like [prioritize], but lower version numbers are considered greater than
+ /// higher version numbers.
+ ///
+ /// This still considers prerelease versions to be lower than non-prerelease
+ /// versions. Pub uses this when downgrading -- it chooses the lowest version
+ /// but still excludes pre-release versions when possible.
+ static int antiprioritize(Version a, Version b) {
+ if (a.isPreRelease && !b.isPreRelease) return -1;
+ if (!a.isPreRelease && b.isPreRelease) return 1;
+
+ return b.compareTo(a);
+ }
+
+ /// The major version number: "1" in "1.2.3".
+ final int major;
+
+ /// The minor version number: "2" in "1.2.3".
+ final int minor;
+
+ /// The patch version number: "3" in "1.2.3".
+ final int patch;
+
+ /// The pre-release identifier: "foo" in "1.2.3-foo".
+ ///
+ /// This is split into a list of components, each of which may be either a
+ /// string or a non-negative integer. It may also be empty, indicating that
+ /// this version has no pre-release identifier.
+ final List<Object> preRelease;
+
+ /// The build identifier: "foo" in "1.2.3+foo".
+ ///
+ /// This is split into a list of components, each of which may be either a
+ /// string or a non-negative integer. It may also be empty, indicating that
+ /// this version has no build identifier.
+ final List<Object> build;
+
+ /// The original string representation of the version number.
+ ///
+ /// This preserves textual artifacts like leading zeros that may be left out
+ /// of the parsed version.
+ final String _text;
+
+ @override
+ Version get min => this;
+ @override
+ Version get max => this;
+ @override
+ bool get includeMin => true;
+ @override
+ bool get includeMax => true;
+
+ Version._(this.major, this.minor, this.patch, String? preRelease,
+ String? build, this._text)
+ : preRelease = preRelease == null ? <Object>[] : _splitParts(preRelease),
+ build = build == null ? [] : _splitParts(build) {
+ if (major < 0) throw ArgumentError('Major version must be non-negative.');
+ if (minor < 0) throw ArgumentError('Minor version must be non-negative.');
+ if (patch < 0) throw ArgumentError('Patch version must be non-negative.');
+ }
+
+ /// Creates a new [Version] object.
+ factory Version(int major, int minor, int patch,
+ {String? pre, String? build}) {
+ var text = '$major.$minor.$patch';
+ if (pre != null) text += '-$pre';
+ if (build != null) text += '+$build';
+
+ return Version._(major, minor, patch, pre, build, text);
+ }
+
+ /// Creates a new [Version] by parsing [text].
+ factory Version.parse(String text) {
+ final match = completeVersion.firstMatch(text);
+ if (match == null) {
+ throw FormatException('Could not parse "$text".');
+ }
+
+ try {
+ var major = int.parse(match[1]!);
+ var minor = int.parse(match[2]!);
+ var patch = int.parse(match[3]!);
+
+ var preRelease = match[5];
+ var build = match[8];
+
+ return Version._(major, minor, patch, preRelease, build, text);
+ } on FormatException {
+ throw FormatException('Could not parse "$text".');
+ }
+ }
+
+ /// Returns the primary version out of [versions].
+ ///
+ /// This is the highest-numbered stable (non-prerelease) version. If there
+ /// are no stable versions, it's just the highest-numbered version.
+ ///
+ /// If [versions] is empty, throws a [StateError].
+ static Version primary(List<Version> versions) {
+ var primary = versions.first;
+ for (var version in versions.skip(1)) {
+ if ((!version.isPreRelease && primary.isPreRelease) ||
+ (version.isPreRelease == primary.isPreRelease && version > primary)) {
+ primary = version;
+ }
+ }
+ return primary;
+ }
+
+ /// Splits a string of dot-delimited identifiers into their component parts.
+ ///
+ /// Identifiers that are numeric are converted to numbers.
+ static List<Object> _splitParts(String text) => text
+ .split('.')
+ .map((part) =>
+ // Return an integer part if possible, otherwise return the string
+ // as-is
+ int.tryParse(part) ?? part)
+ .toList();
+
+ @override
+ bool operator ==(Object other) =>
+ other is Version &&
+ major == other.major &&
+ minor == other.minor &&
+ patch == other.patch &&
+ _equality.equals(preRelease, other.preRelease) &&
+ _equality.equals(build, other.build);
+
+ @override
+ int get hashCode =>
+ major ^
+ minor ^
+ patch ^
+ _equality.hash(preRelease) ^
+ _equality.hash(build);
+
+ bool operator <(Version other) => compareTo(other) < 0;
+ bool operator >(Version other) => compareTo(other) > 0;
+ bool operator <=(Version other) => compareTo(other) <= 0;
+ bool operator >=(Version other) => compareTo(other) >= 0;
+
+ @override
+ bool get isAny => false;
+ @override
+ bool get isEmpty => false;
+
+ /// Whether or not this is a pre-release version.
+ bool get isPreRelease => preRelease.isNotEmpty;
+
+ /// Gets the next major version number that follows this one.
+ ///
+ /// If this version is a pre-release of a major version release (i.e. the
+ /// minor and patch versions are zero), then it just strips the pre-release
+ /// suffix. Otherwise, it increments the major version and resets the minor
+ /// and patch.
+ Version get nextMajor {
+ if (isPreRelease && minor == 0 && patch == 0) {
+ return Version(major, minor, patch);
+ }
+
+ return _incrementMajor();
+ }
+
+ /// Gets the next minor version number that follows this one.
+ ///
+ /// If this version is a pre-release of a minor version release (i.e. the
+ /// patch version is zero), then it just strips the pre-release suffix.
+ /// Otherwise, it increments the minor version and resets the patch.
+ Version get nextMinor {
+ if (isPreRelease && patch == 0) {
+ return Version(major, minor, patch);
+ }
+
+ return _incrementMinor();
+ }
+
+ /// Gets the next patch version number that follows this one.
+ ///
+ /// If this version is a pre-release, then it just strips the pre-release
+ /// suffix. Otherwise, it increments the patch version.
+ Version get nextPatch {
+ if (isPreRelease) {
+ return Version(major, minor, patch);
+ }
+
+ return _incrementPatch();
+ }
+
+ /// Gets the next breaking version number that follows this one.
+ ///
+ /// Increments [major] if it's greater than zero, otherwise [minor], resets
+ /// subsequent digits to zero, and strips any [preRelease] or [build]
+ /// suffix.
+ Version get nextBreaking {
+ if (major == 0) {
+ return _incrementMinor();
+ }
+
+ return _incrementMajor();
+ }
+
+ /// Returns the first possible pre-release of this version.
+ Version get firstPreRelease => Version(major, minor, patch, pre: '0');
+
+ /// Returns whether this is the first possible pre-release of its version.
+ bool get isFirstPreRelease => preRelease.length == 1 && preRelease.first == 0;
+
+ Version _incrementMajor() => Version(major + 1, 0, 0);
+ Version _incrementMinor() => Version(major, minor + 1, 0);
+ Version _incrementPatch() => Version(major, minor, patch + 1);
+
+ /// Tests if [other] matches this version exactly.
+ @override
+ bool allows(Version other) => this == other;
+
+ @override
+ bool allowsAll(VersionConstraint other) => other.isEmpty || other == this;
+
+ @override
+ bool allowsAny(VersionConstraint other) => other.allows(this);
+
+ @override
+ VersionConstraint intersect(VersionConstraint other) =>
+ other.allows(this) ? this : VersionConstraint.empty;
+
+ @override
+ VersionConstraint union(VersionConstraint other) {
+ if (other.allows(this)) return other;
+
+ if (other is VersionRange) {
+ if (other.min == this) {
+ return VersionRange(
+ min: other.min,
+ max: other.max,
+ includeMin: true,
+ includeMax: other.includeMax,
+ alwaysIncludeMaxPreRelease: true);
+ }
+
+ if (other.max == this) {
+ return VersionRange(
+ min: other.min,
+ max: other.max,
+ includeMin: other.includeMin,
+ includeMax: true,
+ alwaysIncludeMaxPreRelease: true);
+ }
+ }
+
+ return VersionConstraint.unionOf([this, other]);
+ }
+
+ @override
+ VersionConstraint difference(VersionConstraint other) =>
+ other.allows(this) ? VersionConstraint.empty : this;
+
+ @override
+ int compareTo(VersionRange other) {
+ if (other is Version) {
+ if (major != other.major) return major.compareTo(other.major);
+ if (minor != other.minor) return minor.compareTo(other.minor);
+ if (patch != other.patch) return patch.compareTo(other.patch);
+
+ // Pre-releases always come before no pre-release string.
+ if (!isPreRelease && other.isPreRelease) return 1;
+ if (!other.isPreRelease && isPreRelease) return -1;
+
+ var comparison = _compareLists(preRelease, other.preRelease);
+ if (comparison != 0) return comparison;
+
+ // Builds always come after no build string.
+ if (build.isEmpty && other.build.isNotEmpty) return -1;
+ if (other.build.isEmpty && build.isNotEmpty) return 1;
+ return _compareLists(build, other.build);
+ } else {
+ return -other.compareTo(this);
+ }
+ }
+
+ /// Get non-canonical string representation of this [Version].
+ ///
+ /// If created with [Version.parse], the string from which the version was
+ /// parsed is returned. Unlike the [canonicalizedVersion] this preserves
+ /// artifacts such as leading zeros.
+ @override
+ String toString() => _text;
+
+ /// Get a canonicalized string representation of this [Version].
+ ///
+ /// Unlike [Version.toString()] this always returns a canonical string
+ /// representation of this [Version].
+ ///
+ /// **Example**
+ /// ```dart
+ /// final v = Version.parse('01.02.03-01.dev+pre.02');
+ ///
+ /// assert(v.toString() == '01.02.03-01.dev+pre.02');
+ /// assert(v.canonicalizedVersion == '1.2.3-1.dev+pre.2');
+ /// assert(Version.parse(v.canonicalizedVersion) == v);
+ /// ```
+ String get canonicalizedVersion => Version(
+ major,
+ minor,
+ patch,
+ pre: preRelease.isNotEmpty ? preRelease.join('.') : null,
+ build: build.isNotEmpty ? build.join('.') : null,
+ ).toString();
+
+ /// Compares a dot-separated component of two versions.
+ ///
+ /// This is used for the pre-release and build version parts. This follows
+ /// Rule 12 of the Semantic Versioning spec (v2.0.0-rc.1).
+ int _compareLists(List<Object> a, List<Object> b) {
+ for (var i = 0; i < math.max(a.length, b.length); i++) {
+ var aPart = (i < a.length) ? a[i] : null;
+ var bPart = (i < b.length) ? b[i] : null;
+
+ if (aPart == bPart) continue;
+
+ // Missing parts come before present ones.
+ if (aPart == null) return -1;
+ if (bPart == null) return 1;
+
+ if (aPart is num) {
+ if (bPart is num) {
+ // Compare two numbers.
+ return aPart.compareTo(bPart);
+ } else {
+ // Numbers come before strings.
+ return -1;
+ }
+ } else {
+ if (bPart is num) {
+ // Strings come after numbers.
+ return 1;
+ } else {
+ // Compare two strings.
+ return (aPart as String).compareTo(bPart as String);
+ }
+ }
+ }
+
+ // The lists are entirely equal.
+ return 0;
+ }
+}
diff --git a/pkgs/pub_semver/lib/src/version_constraint.dart b/pkgs/pub_semver/lib/src/version_constraint.dart
new file mode 100644
index 0000000..948118e
--- /dev/null
+++ b/pkgs/pub_semver/lib/src/version_constraint.dart
@@ -0,0 +1,287 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'patterns.dart';
+import 'utils.dart';
+import 'version.dart';
+import 'version_range.dart';
+import 'version_union.dart';
+
+/// A [VersionConstraint] is a predicate that can determine whether a given
+/// version is valid or not.
+///
+/// For example, a ">= 2.0.0" constraint allows any version that is "2.0.0" or
+/// greater. Version objects themselves implement this to match a specific
+/// version.
+abstract class VersionConstraint {
+ /// A [VersionConstraint] that allows all versions.
+ static VersionConstraint any = VersionRange();
+
+ /// A [VersionConstraint] that allows no versions -- the empty set.
+ static VersionConstraint empty = const _EmptyVersion();
+
+ /// Parses a version constraint.
+ ///
+ /// This string is one of:
+ ///
+ /// * "any". [any] version.
+ /// * "^" followed by a version string. Versions compatible with
+ /// ([VersionConstraint.compatibleWith]) the version.
+ /// * a series of version parts. Each part can be one of:
+ /// * A version string like `1.2.3`. In other words, anything that can be
+ /// parsed by [Version.parse()].
+ /// * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a
+ /// version string.
+ ///
+ /// Whitespace is ignored.
+ ///
+ /// Examples:
+ ///
+ /// any
+ /// ^0.7.2
+ /// ^1.0.0-alpha
+ /// 1.2.3-alpha
+ /// <=5.1.4
+ /// >2.0.4 <= 2.4.6
+ factory VersionConstraint.parse(String text) {
+ var originalText = text;
+
+ void skipWhitespace() {
+ text = text.trim();
+ }
+
+ skipWhitespace();
+
+ // Handle the "any" constraint.
+ if (text == 'any') return any;
+
+ // Try to parse and consume a version number.
+ Version? matchVersion() {
+ var version = startVersion.firstMatch(text);
+ if (version == null) return null;
+
+ text = text.substring(version.end);
+ return Version.parse(version[0]!);
+ }
+
+ // Try to parse and consume a comparison operator followed by a version.
+ VersionRange? matchComparison() {
+ var comparison = startComparison.firstMatch(text);
+ if (comparison == null) return null;
+
+ var op = comparison[0]!;
+ text = text.substring(comparison.end);
+ skipWhitespace();
+
+ var version = matchVersion();
+ if (version == null) {
+ throw FormatException('Expected version number after "$op" in '
+ '"$originalText", got "$text".');
+ }
+
+ return switch (op) {
+ '<=' => VersionRange(max: version, includeMax: true),
+ '<' => VersionRange(max: version, alwaysIncludeMaxPreRelease: true),
+ '>=' => VersionRange(min: version, includeMin: true),
+ '>' => VersionRange(min: version),
+ _ => throw UnsupportedError(op),
+ };
+ }
+
+ // Try to parse the "^" operator followed by a version.
+ VersionConstraint? matchCompatibleWith() {
+ if (!text.startsWith(compatibleWithChar)) return null;
+
+ text = text.substring(compatibleWithChar.length);
+ skipWhitespace();
+
+ var version = matchVersion();
+ if (version == null) {
+ throw FormatException('Expected version number after '
+ '"$compatibleWithChar" in "$originalText", got "$text".');
+ }
+
+ if (text.isNotEmpty) {
+ throw FormatException('Cannot include other constraints with '
+ '"$compatibleWithChar" constraint in "$originalText".');
+ }
+
+ return VersionConstraint.compatibleWith(version);
+ }
+
+ var compatibleWith = matchCompatibleWith();
+ if (compatibleWith != null) return compatibleWith;
+
+ Version? min;
+ var includeMin = false;
+ Version? max;
+ var includeMax = false;
+
+ for (;;) {
+ skipWhitespace();
+
+ if (text.isEmpty) break;
+
+ var newRange = matchVersion() ?? matchComparison();
+ if (newRange == null) {
+ throw FormatException('Could not parse version "$originalText". '
+ 'Unknown text at "$text".');
+ }
+
+ if (newRange.min != null) {
+ if (min == null || newRange.min! > min) {
+ min = newRange.min;
+ includeMin = newRange.includeMin;
+ } else if (newRange.min == min && !newRange.includeMin) {
+ includeMin = false;
+ }
+ }
+
+ if (newRange.max != null) {
+ if (max == null || newRange.max! < max) {
+ max = newRange.max;
+ includeMax = newRange.includeMax;
+ } else if (newRange.max == max && !newRange.includeMax) {
+ includeMax = false;
+ }
+ }
+ }
+
+ if (min == null && max == null) {
+ throw const FormatException('Cannot parse an empty string.');
+ }
+
+ if (min != null && max != null) {
+ if (min > max) return VersionConstraint.empty;
+ if (min == max) {
+ if (includeMin && includeMax) return min;
+ return VersionConstraint.empty;
+ }
+ }
+
+ return VersionRange(
+ min: min, includeMin: includeMin, max: max, includeMax: includeMax);
+ }
+
+ /// Creates a version constraint which allows all versions that are
+ /// backward compatible with [version].
+ ///
+ /// Versions are considered backward compatible with [version] if they
+ /// are greater than or equal to [version], but less than the next breaking
+ /// version ([Version.nextBreaking]) of [version].
+ factory VersionConstraint.compatibleWith(Version version) =>
+ CompatibleWithVersionRange(version);
+
+ /// Creates a new version constraint that is the intersection of
+ /// [constraints].
+ ///
+ /// It only allows versions that all of those constraints allow. If
+ /// constraints is empty, then it returns a VersionConstraint that allows
+ /// all versions.
+ factory VersionConstraint.intersection(
+ Iterable<VersionConstraint> constraints) {
+ var constraint = VersionRange();
+ for (var other in constraints) {
+ constraint = constraint.intersect(other) as VersionRange;
+ }
+ return constraint;
+ }
+
+ /// Creates a new version constraint that is the union of [constraints].
+ ///
+ /// It allows any versions that any of those constraints allows. If
+ /// [constraints] is empty, this returns a constraint that allows no versions.
+ factory VersionConstraint.unionOf(Iterable<VersionConstraint> constraints) {
+ var flattened = constraints.expand((constraint) {
+ if (constraint.isEmpty) return <VersionRange>[];
+ if (constraint is VersionUnion) return constraint.ranges;
+ if (constraint is VersionRange) return [constraint];
+ throw ArgumentError('Unknown VersionConstraint type $constraint.');
+ }).toList();
+
+ if (flattened.isEmpty) return VersionConstraint.empty;
+
+ if (flattened.any((constraint) => constraint.isAny)) {
+ return VersionConstraint.any;
+ }
+
+ flattened.sort();
+
+ var merged = <VersionRange>[];
+ for (var constraint in flattened) {
+ // Merge this constraint with the previous one, but only if they touch.
+ if (merged.isEmpty ||
+ (!merged.last.allowsAny(constraint) &&
+ !areAdjacent(merged.last, constraint))) {
+ merged.add(constraint);
+ } else {
+ merged[merged.length - 1] =
+ merged.last.union(constraint) as VersionRange;
+ }
+ }
+
+ if (merged.length == 1) return merged.single;
+ return VersionUnion.fromRanges(merged);
+ }
+
+ /// Returns `true` if this constraint allows no versions.
+ bool get isEmpty;
+
+ /// Returns `true` if this constraint allows all versions.
+ bool get isAny;
+
+ /// Returns `true` if this constraint allows [version].
+ bool allows(Version version);
+
+ /// Returns `true` if this constraint allows all the versions that [other]
+ /// allows.
+ bool allowsAll(VersionConstraint other);
+
+ /// Returns `true` if this constraint allows any of the versions that [other]
+ /// allows.
+ bool allowsAny(VersionConstraint other);
+
+ /// Returns a [VersionConstraint] that only allows [Version]s allowed by both
+ /// this and [other].
+ VersionConstraint intersect(VersionConstraint other);
+
+ /// Returns a [VersionConstraint] that allows [Version]s allowed by either
+ /// this or [other].
+ VersionConstraint union(VersionConstraint other);
+
+ /// Returns a [VersionConstraint] that allows [Version]s allowed by this but
+ /// not [other].
+ VersionConstraint difference(VersionConstraint other);
+}
+
+class _EmptyVersion implements VersionConstraint {
+ const _EmptyVersion();
+
+ @override
+ bool get isEmpty => true;
+
+ @override
+ bool get isAny => false;
+
+ @override
+ bool allows(Version other) => false;
+
+ @override
+ bool allowsAll(VersionConstraint other) => other.isEmpty;
+
+ @override
+ bool allowsAny(VersionConstraint other) => false;
+
+ @override
+ VersionConstraint intersect(VersionConstraint other) => this;
+
+ @override
+ VersionConstraint union(VersionConstraint other) => other;
+
+ @override
+ VersionConstraint difference(VersionConstraint other) => this;
+
+ @override
+ String toString() => '<empty>';
+}
diff --git a/pkgs/pub_semver/lib/src/version_range.dart b/pkgs/pub_semver/lib/src/version_range.dart
new file mode 100644
index 0000000..6f2ed54
--- /dev/null
+++ b/pkgs/pub_semver/lib/src/version_range.dart
@@ -0,0 +1,476 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'utils.dart';
+import 'version.dart';
+import 'version_constraint.dart';
+import 'version_union.dart';
+
+/// Constrains versions to a fall within a given range.
+///
+/// If there is a minimum, then this only allows versions that are at that
+/// minimum or greater. If there is a maximum, then only versions less than
+/// that are allowed. In other words, this allows `>= min, < max`.
+///
+/// Version ranges are ordered first by their lower bounds, then by their upper
+/// bounds. For example, `>=1.0.0 <2.0.0` is before `>=1.5.0 <2.0.0` is before
+/// `>=1.5.0 <3.0.0`.
+class VersionRange implements Comparable<VersionRange>, VersionConstraint {
+ /// The minimum end of the range.
+ ///
+ /// If [includeMin] is `true`, this will be the minimum allowed version.
+ /// Otherwise, it will be the highest version below the range that is not
+ /// allowed.
+ ///
+ /// This may be `null` in which case the range has no minimum end and allows
+ /// any version less than the maximum.
+ final Version? min;
+
+ /// The maximum end of the range.
+ ///
+ /// If [includeMax] is `true`, this will be the maximum allowed version.
+ /// Otherwise, it will be the lowest version above the range that is not
+ /// allowed.
+ ///
+ /// This may be `null` in which case the range has no maximum end and allows
+ /// any version greater than the minimum.
+ final Version? max;
+
+ /// If `true` then [min] is allowed by the range.
+ final bool includeMin;
+
+ /// If `true`, then [max] is allowed by the range.
+ final bool includeMax;
+
+ /// Creates a new version range from [min] to [max], either inclusive or
+ /// exclusive.
+ ///
+ /// If it is an error if [min] is greater than [max].
+ ///
+ /// Either [max] or [min] may be omitted to not clamp the range at that end.
+ /// If both are omitted, the range allows all versions.
+ ///
+ /// If [includeMin] is `true`, then the minimum end of the range is inclusive.
+ /// Likewise, passing [includeMax] as `true` makes the upper end inclusive.
+ ///
+ /// If [alwaysIncludeMaxPreRelease] is `true`, this will always include
+ /// pre-release versions of an exclusive [max]. Otherwise, it will use the
+ /// default behavior for pre-release versions of [max].
+ factory VersionRange(
+ {Version? min,
+ Version? max,
+ bool includeMin = false,
+ bool includeMax = false,
+ bool alwaysIncludeMaxPreRelease = false}) {
+ if (min != null && max != null && min > max) {
+ throw ArgumentError(
+ 'Minimum version ("$min") must be less than maximum ("$max").');
+ }
+
+ if (!alwaysIncludeMaxPreRelease &&
+ !includeMax &&
+ max != null &&
+ !max.isPreRelease &&
+ max.build.isEmpty &&
+ (min == null ||
+ !min.isPreRelease ||
+ !equalsWithoutPreRelease(min, max))) {
+ max = max.firstPreRelease;
+ }
+
+ return VersionRange._(min, max, includeMin, includeMax);
+ }
+
+ VersionRange._(this.min, this.max, this.includeMin, this.includeMax);
+
+ @override
+ bool operator ==(Object other) {
+ if (other is! VersionRange) return false;
+
+ return min == other.min &&
+ max == other.max &&
+ includeMin == other.includeMin &&
+ includeMax == other.includeMax;
+ }
+
+ @override
+ int get hashCode =>
+ min.hashCode ^
+ (max.hashCode * 3) ^
+ (includeMin.hashCode * 5) ^
+ (includeMax.hashCode * 7);
+
+ @override
+ bool get isEmpty => false;
+
+ @override
+ bool get isAny => min == null && max == null;
+
+ /// Tests if [other] falls within this version range.
+ @override
+ bool allows(Version other) {
+ if (min != null) {
+ if (other < min!) return false;
+ if (!includeMin && other == min) return false;
+ }
+
+ if (max != null) {
+ if (other > max!) return false;
+ if (!includeMax && other == max) return false;
+ }
+
+ return true;
+ }
+
+ @override
+ bool allowsAll(VersionConstraint other) {
+ if (other.isEmpty) return true;
+ if (other is Version) return allows(other);
+
+ if (other is VersionUnion) {
+ return other.ranges.every(allowsAll);
+ }
+
+ if (other is VersionRange) {
+ return !allowsLower(other, this) && !allowsHigher(other, this);
+ }
+
+ throw ArgumentError('Unknown VersionConstraint type $other.');
+ }
+
+ @override
+ bool allowsAny(VersionConstraint other) {
+ if (other.isEmpty) return false;
+ if (other is Version) return allows(other);
+
+ if (other is VersionUnion) {
+ return other.ranges.any(allowsAny);
+ }
+
+ if (other is VersionRange) {
+ return !strictlyLower(other, this) && !strictlyHigher(other, this);
+ }
+
+ throw ArgumentError('Unknown VersionConstraint type $other.');
+ }
+
+ @override
+ VersionConstraint intersect(VersionConstraint other) {
+ if (other.isEmpty) return other;
+ if (other is VersionUnion) return other.intersect(this);
+
+ // A range and a Version just yields the version if it's in the range.
+ if (other is Version) {
+ return allows(other) ? other : VersionConstraint.empty;
+ }
+
+ if (other is VersionRange) {
+ // Intersect the two ranges.
+ Version? intersectMin;
+ bool intersectIncludeMin;
+ if (allowsLower(this, other)) {
+ if (strictlyLower(this, other)) return VersionConstraint.empty;
+ intersectMin = other.min;
+ intersectIncludeMin = other.includeMin;
+ } else {
+ if (strictlyLower(other, this)) return VersionConstraint.empty;
+ intersectMin = min;
+ intersectIncludeMin = includeMin;
+ }
+
+ Version? intersectMax;
+ bool intersectIncludeMax;
+ if (allowsHigher(this, other)) {
+ intersectMax = other.max;
+ intersectIncludeMax = other.includeMax;
+ } else {
+ intersectMax = max;
+ intersectIncludeMax = includeMax;
+ }
+
+ if (intersectMin == null && intersectMax == null) {
+ // Open range.
+ return VersionRange();
+ }
+
+ // If the range is just a single version.
+ if (intersectMin == intersectMax) {
+ // Because we already verified that the lower range isn't strictly
+ // lower, there must be some overlap.
+ assert(intersectIncludeMin && intersectIncludeMax);
+ return intersectMin!;
+ }
+
+ // If we got here, there is an actual range.
+ return VersionRange(
+ min: intersectMin,
+ max: intersectMax,
+ includeMin: intersectIncludeMin,
+ includeMax: intersectIncludeMax,
+ alwaysIncludeMaxPreRelease: true);
+ }
+
+ throw ArgumentError('Unknown VersionConstraint type $other.');
+ }
+
+ @override
+ VersionConstraint union(VersionConstraint other) {
+ if (other is Version) {
+ if (allows(other)) return this;
+
+ if (other == min) {
+ return VersionRange(
+ min: min,
+ max: max,
+ includeMin: true,
+ includeMax: includeMax,
+ alwaysIncludeMaxPreRelease: true);
+ }
+
+ if (other == max) {
+ return VersionRange(
+ min: min,
+ max: max,
+ includeMin: includeMin,
+ includeMax: true,
+ alwaysIncludeMaxPreRelease: true);
+ }
+
+ return VersionConstraint.unionOf([this, other]);
+ }
+
+ if (other is VersionRange) {
+ // If the two ranges don't overlap, we won't be able to create a single
+ // VersionRange for both of them.
+ var edgesTouch = (max != null &&
+ max == other.min &&
+ (includeMax || other.includeMin)) ||
+ (min != null && min == other.max && (includeMin || other.includeMax));
+ if (!edgesTouch && !allowsAny(other)) {
+ return VersionConstraint.unionOf([this, other]);
+ }
+
+ Version? unionMin;
+ bool unionIncludeMin;
+ if (allowsLower(this, other)) {
+ unionMin = min;
+ unionIncludeMin = includeMin;
+ } else {
+ unionMin = other.min;
+ unionIncludeMin = other.includeMin;
+ }
+
+ Version? unionMax;
+ bool unionIncludeMax;
+ if (allowsHigher(this, other)) {
+ unionMax = max;
+ unionIncludeMax = includeMax;
+ } else {
+ unionMax = other.max;
+ unionIncludeMax = other.includeMax;
+ }
+
+ return VersionRange(
+ min: unionMin,
+ max: unionMax,
+ includeMin: unionIncludeMin,
+ includeMax: unionIncludeMax,
+ alwaysIncludeMaxPreRelease: true);
+ }
+
+ return VersionConstraint.unionOf([this, other]);
+ }
+
+ @override
+ VersionConstraint difference(VersionConstraint other) {
+ if (other.isEmpty) return this;
+
+ if (other is Version) {
+ if (!allows(other)) return this;
+
+ if (other == min) {
+ if (!includeMin) return this;
+ return VersionRange(
+ min: min,
+ max: max,
+ includeMax: includeMax,
+ alwaysIncludeMaxPreRelease: true);
+ }
+
+ if (other == max) {
+ if (!includeMax) return this;
+ return VersionRange(
+ min: min,
+ max: max,
+ includeMin: includeMin,
+ alwaysIncludeMaxPreRelease: true);
+ }
+
+ return VersionUnion.fromRanges([
+ VersionRange(
+ min: min,
+ max: other,
+ includeMin: includeMin,
+ alwaysIncludeMaxPreRelease: true),
+ VersionRange(
+ min: other,
+ max: max,
+ includeMax: includeMax,
+ alwaysIncludeMaxPreRelease: true)
+ ]);
+ } else if (other is VersionRange) {
+ if (!allowsAny(other)) return this;
+
+ VersionRange? before;
+ if (!allowsLower(this, other)) {
+ before = null;
+ } else if (min == other.min) {
+ assert(includeMin && !other.includeMin);
+ assert(min != null);
+ before = min;
+ } else {
+ before = VersionRange(
+ min: min,
+ max: other.min,
+ includeMin: includeMin,
+ includeMax: !other.includeMin,
+ alwaysIncludeMaxPreRelease: true);
+ }
+
+ VersionRange? after;
+ if (!allowsHigher(this, other)) {
+ after = null;
+ } else if (max == other.max) {
+ assert(includeMax && !other.includeMax);
+ assert(max != null);
+ after = max;
+ } else {
+ after = VersionRange(
+ min: other.max,
+ max: max,
+ includeMin: !other.includeMax,
+ includeMax: includeMax,
+ alwaysIncludeMaxPreRelease: true);
+ }
+
+ if (before == null && after == null) return VersionConstraint.empty;
+ if (before == null) return after!;
+ if (after == null) return before;
+ return VersionUnion.fromRanges([before, after]);
+ } else if (other is VersionUnion) {
+ var ranges = <VersionRange>[];
+ var current = this;
+
+ for (var range in other.ranges) {
+ // Skip any ranges that are strictly lower than [current].
+ if (strictlyLower(range, current)) continue;
+
+ // If we reach a range strictly higher than [current], no more ranges
+ // will be relevant so we can bail early.
+ if (strictlyHigher(range, current)) break;
+
+ var difference = current.difference(range);
+ if (difference.isEmpty) {
+ return VersionConstraint.empty;
+ } else if (difference is VersionUnion) {
+ // If [range] split [current] in half, we only need to continue
+ // checking future ranges against the latter half.
+ assert(difference.ranges.length == 2);
+ ranges.add(difference.ranges.first);
+ current = difference.ranges.last;
+ } else {
+ current = difference as VersionRange;
+ }
+ }
+
+ if (ranges.isEmpty) return current;
+ return VersionUnion.fromRanges(ranges..add(current));
+ }
+
+ throw ArgumentError('Unknown VersionConstraint type $other.');
+ }
+
+ @override
+ int compareTo(VersionRange other) {
+ if (min == null) {
+ if (other.min == null) return _compareMax(other);
+ return -1;
+ } else if (other.min == null) {
+ return 1;
+ }
+
+ var result = min!.compareTo(other.min!);
+ if (result != 0) return result;
+ if (includeMin != other.includeMin) return includeMin ? -1 : 1;
+
+ return _compareMax(other);
+ }
+
+ /// Compares the maximum values of `this` and [other].
+ int _compareMax(VersionRange other) {
+ if (max == null) {
+ if (other.max == null) return 0;
+ return 1;
+ } else if (other.max == null) {
+ return -1;
+ }
+
+ var result = max!.compareTo(other.max!);
+ if (result != 0) return result;
+ if (includeMax != other.includeMax) return includeMax ? 1 : -1;
+ return 0;
+ }
+
+ @override
+ String toString() {
+ var buffer = StringBuffer();
+
+ final min = this.min;
+ if (min != null) {
+ buffer
+ ..write(includeMin ? '>=' : '>')
+ ..write(min);
+ }
+
+ final max = this.max;
+
+ if (max != null) {
+ if (min != null) buffer.write(' ');
+ if (includeMax) {
+ buffer
+ ..write('<=')
+ ..write(max);
+ } else {
+ buffer.write('<');
+ if (max.isFirstPreRelease) {
+ // Since `"<$max"` would parse the same as `"<$max-0"`, we just emit
+ // `<$max` to avoid confusing "-0" suffixes.
+ buffer.write('${max.major}.${max.minor}.${max.patch}');
+ } else {
+ buffer.write(max);
+
+ // If `">=$min <$max"` would parse as `">=$min <$max-0"`, add `-*` to
+ // indicate that actually does allow pre-release versions.
+ var minIsPreReleaseOfMax = min != null &&
+ min.isPreRelease &&
+ equalsWithoutPreRelease(min, max);
+ if (!max.isPreRelease && max.build.isEmpty && !minIsPreReleaseOfMax) {
+ buffer.write('-∞');
+ }
+ }
+ }
+ }
+
+ if (min == null && max == null) buffer.write('any');
+ return buffer.toString();
+ }
+}
+
+class CompatibleWithVersionRange extends VersionRange {
+ CompatibleWithVersionRange(Version version)
+ : super._(version, version.nextBreaking.firstPreRelease, true, false);
+
+ @override
+ String toString() => '^$min';
+}
diff --git a/pkgs/pub_semver/lib/src/version_union.dart b/pkgs/pub_semver/lib/src/version_union.dart
new file mode 100644
index 0000000..844d3b8
--- /dev/null
+++ b/pkgs/pub_semver/lib/src/version_union.dart
@@ -0,0 +1,224 @@
+// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:collection/collection.dart';
+
+import 'utils.dart';
+import 'version.dart';
+import 'version_constraint.dart';
+import 'version_range.dart';
+
+/// A version constraint representing a union of multiple disjoint version
+/// ranges.
+///
+/// An instance of this will only be created if the version can't be represented
+/// as a non-compound value.
+class VersionUnion implements VersionConstraint {
+ /// The constraints that compose this union.
+ ///
+ /// This list has two invariants:
+ ///
+ /// * Its contents are sorted using the standard ordering of [VersionRange]s.
+ /// * Its contents are disjoint and non-adjacent. In other words, for any two
+ /// constraints next to each other in the list, there's some version between
+ /// those constraints that they don't match.
+ final List<VersionRange> ranges;
+
+ @override
+ bool get isEmpty => false;
+
+ @override
+ bool get isAny => false;
+
+ /// Creates a union from a list of ranges with no pre-processing.
+ ///
+ /// It's up to the caller to ensure that the invariants described in [ranges]
+ /// are maintained. They are not verified by this constructor. To
+ /// automatically ensure that they're maintained, use
+ /// [VersionConstraint.unionOf] instead.
+ VersionUnion.fromRanges(this.ranges);
+
+ @override
+ bool allows(Version version) =>
+ ranges.any((constraint) => constraint.allows(version));
+
+ @override
+ bool allowsAll(VersionConstraint other) {
+ var ourRanges = ranges.iterator;
+ var theirRanges = _rangesFor(other).iterator;
+
+ // Because both lists of ranges are ordered by minimum version, we can
+ // safely move through them linearly here.
+ var ourRangesMoved = ourRanges.moveNext();
+ var theirRangesMoved = theirRanges.moveNext();
+ while (ourRangesMoved && theirRangesMoved) {
+ if (ourRanges.current.allowsAll(theirRanges.current)) {
+ theirRangesMoved = theirRanges.moveNext();
+ } else {
+ ourRangesMoved = ourRanges.moveNext();
+ }
+ }
+
+ // If our ranges have allowed all of their ranges, we'll have consumed all
+ // of them.
+ return !theirRangesMoved;
+ }
+
+ @override
+ bool allowsAny(VersionConstraint other) {
+ var ourRanges = ranges.iterator;
+ var theirRanges = _rangesFor(other).iterator;
+
+ // Because both lists of ranges are ordered by minimum version, we can
+ // safely move through them linearly here.
+ var ourRangesMoved = ourRanges.moveNext();
+ var theirRangesMoved = theirRanges.moveNext();
+ while (ourRangesMoved && theirRangesMoved) {
+ if (ourRanges.current.allowsAny(theirRanges.current)) {
+ return true;
+ }
+
+ // Move the constraint with the lower max value forward. This ensures that
+ // we keep both lists in sync as much as possible.
+ if (allowsHigher(theirRanges.current, ourRanges.current)) {
+ ourRangesMoved = ourRanges.moveNext();
+ } else {
+ theirRangesMoved = theirRanges.moveNext();
+ }
+ }
+
+ return false;
+ }
+
+ @override
+ VersionConstraint intersect(VersionConstraint other) {
+ var ourRanges = ranges.iterator;
+ var theirRanges = _rangesFor(other).iterator;
+
+ // Because both lists of ranges are ordered by minimum version, we can
+ // safely move through them linearly here.
+ var newRanges = <VersionRange>[];
+ var ourRangesMoved = ourRanges.moveNext();
+ var theirRangesMoved = theirRanges.moveNext();
+ while (ourRangesMoved && theirRangesMoved) {
+ var intersection = ourRanges.current.intersect(theirRanges.current);
+
+ if (!intersection.isEmpty) newRanges.add(intersection as VersionRange);
+
+ // Move the constraint with the lower max value forward. This ensures that
+ // we keep both lists in sync as much as possible, and that large ranges
+ // have a chance to match multiple small ranges that they contain.
+ if (allowsHigher(theirRanges.current, ourRanges.current)) {
+ ourRangesMoved = ourRanges.moveNext();
+ } else {
+ theirRangesMoved = theirRanges.moveNext();
+ }
+ }
+
+ if (newRanges.isEmpty) return VersionConstraint.empty;
+ if (newRanges.length == 1) return newRanges.single;
+
+ return VersionUnion.fromRanges(newRanges);
+ }
+
+ @override
+ VersionConstraint difference(VersionConstraint other) {
+ var ourRanges = ranges.iterator;
+ var theirRanges = _rangesFor(other).iterator;
+
+ var newRanges = <VersionRange>[];
+ ourRanges.moveNext();
+ theirRanges.moveNext();
+ var current = ourRanges.current;
+
+ bool theirNextRange() {
+ if (theirRanges.moveNext()) return true;
+
+ // If there are no more of their ranges, none of the rest of our ranges
+ // need to be subtracted so we can add them as-is.
+ newRanges.add(current);
+ while (ourRanges.moveNext()) {
+ newRanges.add(ourRanges.current);
+ }
+ return false;
+ }
+
+ bool ourNextRange({bool includeCurrent = true}) {
+ if (includeCurrent) newRanges.add(current);
+ if (!ourRanges.moveNext()) return false;
+ current = ourRanges.current;
+ return true;
+ }
+
+ for (;;) {
+ // If the current ranges are disjoint, move the lowest one forward.
+ if (strictlyLower(theirRanges.current, current)) {
+ if (!theirNextRange()) break;
+ continue;
+ }
+
+ if (strictlyHigher(theirRanges.current, current)) {
+ if (!ourNextRange()) break;
+ continue;
+ }
+
+ // If we're here, we know [theirRanges.current] overlaps [current].
+ var difference = current.difference(theirRanges.current);
+ if (difference is VersionUnion) {
+ // If their range split [current] in half, we only need to continue
+ // checking future ranges against the latter half.
+ assert(difference.ranges.length == 2);
+ newRanges.add(difference.ranges.first);
+ current = difference.ranges.last;
+
+ // Since their range split [current], it definitely doesn't allow higher
+ // versions, so we should move their ranges forward.
+ if (!theirNextRange()) break;
+ } else if (difference.isEmpty) {
+ if (!ourNextRange(includeCurrent: false)) break;
+ } else {
+ current = difference as VersionRange;
+
+ // Move the constraint with the lower max value forward. This ensures
+ // that we keep both lists in sync as much as possible, and that large
+ // ranges have a chance to subtract or be subtracted by multiple small
+ // ranges that they contain.
+ if (allowsHigher(current, theirRanges.current)) {
+ if (!theirNextRange()) break;
+ } else {
+ if (!ourNextRange()) break;
+ }
+ }
+ }
+
+ if (newRanges.isEmpty) return VersionConstraint.empty;
+ if (newRanges.length == 1) return newRanges.single;
+ return VersionUnion.fromRanges(newRanges);
+ }
+
+ /// Returns [constraint] as a list of ranges.
+ ///
+ /// This is used to normalize ranges of various types.
+ List<VersionRange> _rangesFor(VersionConstraint constraint) {
+ if (constraint.isEmpty) return [];
+ if (constraint is VersionUnion) return constraint.ranges;
+ if (constraint is VersionRange) return [constraint];
+ throw ArgumentError('Unknown VersionConstraint type $constraint.');
+ }
+
+ @override
+ VersionConstraint union(VersionConstraint other) =>
+ VersionConstraint.unionOf([this, other]);
+
+ @override
+ bool operator ==(Object other) =>
+ other is VersionUnion &&
+ const ListEquality<VersionRange>().equals(ranges, other.ranges);
+
+ @override
+ int get hashCode => const ListEquality<VersionRange>().hash(ranges);
+
+ @override
+ String toString() => ranges.join(' or ');
+}
diff --git a/pkgs/pub_semver/pubspec.yaml b/pkgs/pub_semver/pubspec.yaml
new file mode 100644
index 0000000..290fb92
--- /dev/null
+++ b/pkgs/pub_semver/pubspec.yaml
@@ -0,0 +1,20 @@
+name: pub_semver
+version: 2.1.5
+description: >-
+ Versions and version constraints implementing pub's versioning policy. This
+ is very similar to vanilla semver, with a few corner cases.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/pub_semver
+topics:
+ - dart-pub
+ - semver
+
+environment:
+ sdk: ^3.4.0
+
+dependencies:
+ collection: ^1.15.0
+ meta: ^1.3.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
+ test: ^1.16.0
diff --git a/pkgs/pub_semver/test/utils.dart b/pkgs/pub_semver/test/utils.dart
new file mode 100644
index 0000000..bd7aa8f
--- /dev/null
+++ b/pkgs/pub_semver/test/utils.dart
@@ -0,0 +1,123 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+/// Some stock example versions to use in tests.
+final v003 = Version.parse('0.0.3');
+final v010 = Version.parse('0.1.0');
+final v072 = Version.parse('0.7.2');
+final v080 = Version.parse('0.8.0');
+final v114 = Version.parse('1.1.4');
+final v123 = Version.parse('1.2.3');
+final v124 = Version.parse('1.2.4');
+final v130 = Version.parse('1.3.0');
+final v140 = Version.parse('1.4.0');
+final v200 = Version.parse('2.0.0');
+final v201 = Version.parse('2.0.1');
+final v234 = Version.parse('2.3.4');
+final v250 = Version.parse('2.5.0');
+final v300 = Version.parse('3.0.0');
+
+/// A range that allows pre-release versions of its max version.
+final includeMaxPreReleaseRange =
+ VersionRange(max: v200, alwaysIncludeMaxPreRelease: true);
+
+/// A [Matcher] that tests if a [VersionConstraint] allows or does not allow a
+/// given list of [Version]s.
+class _VersionConstraintMatcher implements Matcher {
+ final List<Version> _expected;
+ final bool _allow;
+
+ _VersionConstraintMatcher(this._expected, this._allow);
+
+ @override
+ bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
+ (item is VersionConstraint) &&
+ _expected.every((version) => item.allows(version) == _allow);
+
+ @override
+ Description describe(Description description) {
+ description.addAll(' ${_allow ? "allows" : "does not allow"} versions ',
+ ', ', '', _expected);
+ return description;
+ }
+
+ @override
+ Description describeMismatch(dynamic item, Description mismatchDescription,
+ Map<dynamic, dynamic> matchState, bool verbose) {
+ if (item is! VersionConstraint) {
+ mismatchDescription.add('was not a VersionConstraint');
+ return mismatchDescription;
+ }
+
+ var first = true;
+ for (var version in _expected) {
+ if (item.allows(version) != _allow) {
+ if (first) {
+ if (_allow) {
+ mismatchDescription.addDescriptionOf(item).add(' did not allow ');
+ } else {
+ mismatchDescription.addDescriptionOf(item).add(' allowed ');
+ }
+ } else {
+ mismatchDescription.add(' and ');
+ }
+ first = false;
+
+ mismatchDescription.add(version.toString());
+ }
+ }
+
+ return mismatchDescription;
+ }
+}
+
+/// Gets a [Matcher] that validates that a [VersionConstraint] allows all
+/// given versions.
+Matcher allows(Version v1,
+ [Version? v2,
+ Version? v3,
+ Version? v4,
+ Version? v5,
+ Version? v6,
+ Version? v7,
+ Version? v8]) {
+ var versions = _makeVersionList(v1, v2, v3, v4, v5, v6, v7, v8);
+ return _VersionConstraintMatcher(versions, true);
+}
+
+/// Gets a [Matcher] that validates that a [VersionConstraint] allows none of
+/// the given versions.
+Matcher doesNotAllow(Version v1,
+ [Version? v2,
+ Version? v3,
+ Version? v4,
+ Version? v5,
+ Version? v6,
+ Version? v7,
+ Version? v8]) {
+ var versions = _makeVersionList(v1, v2, v3, v4, v5, v6, v7, v8);
+ return _VersionConstraintMatcher(versions, false);
+}
+
+List<Version> _makeVersionList(Version v1,
+ [Version? v2,
+ Version? v3,
+ Version? v4,
+ Version? v5,
+ Version? v6,
+ Version? v7,
+ Version? v8]) {
+ var versions = [v1];
+ if (v2 != null) versions.add(v2);
+ if (v3 != null) versions.add(v3);
+ if (v4 != null) versions.add(v4);
+ if (v5 != null) versions.add(v5);
+ if (v6 != null) versions.add(v6);
+ if (v7 != null) versions.add(v7);
+ if (v8 != null) versions.add(v8);
+ return versions;
+}
diff --git a/pkgs/pub_semver/test/version_constraint_test.dart b/pkgs/pub_semver/test/version_constraint_test.dart
new file mode 100644
index 0000000..4fbcbe0
--- /dev/null
+++ b/pkgs/pub_semver/test/version_constraint_test.dart
@@ -0,0 +1,185 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ test('any', () {
+ expect(VersionConstraint.any.isAny, isTrue);
+ expect(
+ VersionConstraint.any,
+ allows(Version.parse('0.0.0-blah'), Version.parse('1.2.3'),
+ Version.parse('12345.678.90')));
+ });
+
+ test('empty', () {
+ expect(VersionConstraint.empty.isEmpty, isTrue);
+ expect(VersionConstraint.empty.isAny, isFalse);
+ expect(
+ VersionConstraint.empty,
+ doesNotAllow(Version.parse('0.0.0-blah'), Version.parse('1.2.3'),
+ Version.parse('12345.678.90')));
+ });
+
+ group('parse()', () {
+ test('parses an exact version', () {
+ var constraint = VersionConstraint.parse('1.2.3-alpha');
+
+ expect(constraint is Version, isTrue);
+ expect(constraint, equals(Version(1, 2, 3, pre: 'alpha')));
+ });
+
+ test('parses "any"', () {
+ var constraint = VersionConstraint.parse('any');
+
+ expect(
+ constraint,
+ allows(Version.parse('0.0.0'), Version.parse('1.2.3'),
+ Version.parse('12345.678.90')));
+ });
+
+ test('parses a ">" minimum version', () {
+ var constraint = VersionConstraint.parse('>1.2.3');
+
+ expect(constraint,
+ allows(Version.parse('1.2.3+foo'), Version.parse('1.2.4')));
+ expect(
+ constraint,
+ doesNotAllow(Version.parse('1.2.1'), Version.parse('1.2.3-build'),
+ Version.parse('1.2.3')));
+ });
+
+ test('parses a ">=" minimum version', () {
+ var constraint = VersionConstraint.parse('>=1.2.3');
+
+ expect(
+ constraint,
+ allows(Version.parse('1.2.3'), Version.parse('1.2.3+foo'),
+ Version.parse('1.2.4')));
+ expect(constraint,
+ doesNotAllow(Version.parse('1.2.1'), Version.parse('1.2.3-build')));
+ });
+
+ test('parses a "<" maximum version', () {
+ var constraint = VersionConstraint.parse('<1.2.3');
+
+ expect(constraint,
+ allows(Version.parse('1.2.1'), Version.parse('1.2.2+foo')));
+ expect(
+ constraint,
+ doesNotAllow(Version.parse('1.2.3'), Version.parse('1.2.3+foo'),
+ Version.parse('1.2.4')));
+ });
+
+ test('parses a "<=" maximum version', () {
+ var constraint = VersionConstraint.parse('<=1.2.3');
+
+ expect(
+ constraint,
+ allows(Version.parse('1.2.1'), Version.parse('1.2.3-build'),
+ Version.parse('1.2.3')));
+ expect(constraint,
+ doesNotAllow(Version.parse('1.2.3+foo'), Version.parse('1.2.4')));
+ });
+
+ test('parses a series of space-separated constraints', () {
+ var constraint = VersionConstraint.parse('>1.0.0 >=1.2.3 <1.3.0');
+
+ expect(
+ constraint, allows(Version.parse('1.2.3'), Version.parse('1.2.5')));
+ expect(
+ constraint,
+ doesNotAllow(Version.parse('1.2.3-pre'), Version.parse('1.3.0'),
+ Version.parse('3.4.5')));
+ });
+
+ test('parses a pre-release-only constraint', () {
+ var constraint = VersionConstraint.parse('>=1.0.0-dev.2 <1.0.0');
+ expect(constraint,
+ allows(Version.parse('1.0.0-dev.2'), Version.parse('1.0.0-dev.3')));
+ expect(constraint,
+ doesNotAllow(Version.parse('1.0.0-dev.1'), Version.parse('1.0.0')));
+ });
+
+ test('ignores whitespace around comparison operators', () {
+ var constraint = VersionConstraint.parse(' >1.0.0>=1.2.3 < 1.3.0');
+
+ expect(
+ constraint, allows(Version.parse('1.2.3'), Version.parse('1.2.5')));
+ expect(
+ constraint,
+ doesNotAllow(Version.parse('1.2.3-pre'), Version.parse('1.3.0'),
+ Version.parse('3.4.5')));
+ });
+
+ test('does not allow "any" to be mixed with other constraints', () {
+ expect(() => VersionConstraint.parse('any 1.0.0'), throwsFormatException);
+ });
+
+ test('parses a "^" version', () {
+ expect(VersionConstraint.parse('^0.0.3'),
+ equals(VersionConstraint.compatibleWith(v003)));
+
+ expect(VersionConstraint.parse('^0.7.2'),
+ equals(VersionConstraint.compatibleWith(v072)));
+
+ expect(VersionConstraint.parse('^1.2.3'),
+ equals(VersionConstraint.compatibleWith(v123)));
+
+ var min = Version.parse('0.7.2-pre+1');
+ expect(VersionConstraint.parse('^0.7.2-pre+1'),
+ equals(VersionConstraint.compatibleWith(min)));
+ });
+
+ test('does not allow "^" to be mixed with other constraints', () {
+ expect(() => VersionConstraint.parse('>=1.2.3 ^1.0.0'),
+ throwsFormatException);
+ expect(() => VersionConstraint.parse('^1.0.0 <1.2.3'),
+ throwsFormatException);
+ });
+
+ test('ignores whitespace around "^"', () {
+ var constraint = VersionConstraint.parse(' ^ 1.2.3 ');
+
+ expect(constraint, equals(VersionConstraint.compatibleWith(v123)));
+ });
+
+ test('throws FormatException on a bad string', () {
+ var bad = [
+ '', ' ', // Empty string.
+ 'foo', // Bad text.
+ '>foo', // Bad text after operator.
+ '^foo', // Bad text after "^".
+ '1.0.0 foo', '1.0.0foo', // Bad text after version.
+ 'anything', // Bad text after "any".
+ '<>1.0.0', // Multiple operators.
+ '1.0.0<' // Trailing operator.
+ ];
+
+ for (var text in bad) {
+ expect(() => VersionConstraint.parse(text), throwsFormatException);
+ }
+ });
+ });
+
+ group('compatibleWith()', () {
+ test('returns the range of compatible versions', () {
+ var constraint = VersionConstraint.compatibleWith(v072);
+
+ expect(
+ constraint,
+ equals(VersionRange(
+ min: v072, includeMin: true, max: v072.nextBreaking)));
+ });
+
+ test('toString() uses "^"', () {
+ var constraint = VersionConstraint.compatibleWith(v072);
+
+ expect(constraint.toString(), equals('^0.7.2'));
+ });
+ });
+}
diff --git a/pkgs/pub_semver/test/version_range_test.dart b/pkgs/pub_semver/test/version_range_test.dart
new file mode 100644
index 0000000..5978df0
--- /dev/null
+++ b/pkgs/pub_semver/test/version_range_test.dart
@@ -0,0 +1,998 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ group('constructor', () {
+ test('takes a min and max', () {
+ var range = VersionRange(min: v123, max: v124);
+ expect(range.isAny, isFalse);
+ expect(range.min, equals(v123));
+ expect(range.max, equals(v124.firstPreRelease));
+ });
+
+ group("doesn't make the max a pre-release if", () {
+ test("it's already a pre-release", () {
+ expect(VersionRange(max: Version.parse('1.2.4-pre')).max,
+ equals(Version.parse('1.2.4-pre')));
+ });
+
+ test('includeMax is true', () {
+ expect(VersionRange(max: v124, includeMax: true).max, equals(v124));
+ });
+
+ test('min is a prerelease of max', () {
+ expect(VersionRange(min: Version.parse('1.2.4-pre'), max: v124).max,
+ equals(v124));
+ });
+
+ test('max has a build identifier', () {
+ expect(VersionRange(max: Version.parse('1.2.4+1')).max,
+ equals(Version.parse('1.2.4+1')));
+ });
+ });
+
+ test('allows omitting max', () {
+ var range = VersionRange(min: v123);
+ expect(range.isAny, isFalse);
+ expect(range.min, equals(v123));
+ expect(range.max, isNull);
+ });
+
+ test('allows omitting min and max', () {
+ var range = VersionRange();
+ expect(range.isAny, isTrue);
+ expect(range.min, isNull);
+ expect(range.max, isNull);
+ });
+
+ test('takes includeMin', () {
+ var range = VersionRange(min: v123, includeMin: true);
+ expect(range.includeMin, isTrue);
+ });
+
+ test('includeMin defaults to false if omitted', () {
+ var range = VersionRange(min: v123);
+ expect(range.includeMin, isFalse);
+ });
+
+ test('takes includeMax', () {
+ var range = VersionRange(max: v123, includeMax: true);
+ expect(range.includeMax, isTrue);
+ });
+
+ test('includeMax defaults to false if omitted', () {
+ var range = VersionRange(max: v123);
+ expect(range.includeMax, isFalse);
+ });
+
+ test('throws if min > max', () {
+ expect(() => VersionRange(min: v124, max: v123), throwsArgumentError);
+ });
+ });
+
+ group('allows()', () {
+ test('version must be greater than min', () {
+ var range = VersionRange(min: v123);
+
+ expect(range, allows(Version.parse('1.3.3'), Version.parse('2.3.3')));
+ expect(
+ range, doesNotAllow(Version.parse('1.2.2'), Version.parse('1.2.3')));
+ });
+
+ test('version must be min or greater if includeMin', () {
+ var range = VersionRange(min: v123, includeMin: true);
+
+ expect(
+ range,
+ allows(Version.parse('1.2.3'), Version.parse('1.3.3'),
+ Version.parse('2.3.3')));
+ expect(range, doesNotAllow(Version.parse('1.2.2')));
+ });
+
+ test('pre-release versions of inclusive min are excluded', () {
+ var range = VersionRange(min: v123, includeMin: true);
+
+ expect(range, allows(Version.parse('1.2.4-dev')));
+ expect(range, doesNotAllow(Version.parse('1.2.3-dev')));
+ });
+
+ test('version must be less than max', () {
+ var range = VersionRange(max: v234);
+
+ expect(range, allows(Version.parse('2.3.3')));
+ expect(
+ range, doesNotAllow(Version.parse('2.3.4'), Version.parse('2.4.3')));
+ });
+
+ test('pre-release versions of non-pre-release max are excluded', () {
+ var range = VersionRange(max: v234);
+
+ expect(range, allows(Version.parse('2.3.3')));
+ expect(range,
+ doesNotAllow(Version.parse('2.3.4-dev'), Version.parse('2.3.4')));
+ });
+
+ test(
+ 'pre-release versions of non-pre-release max are included if min is a '
+ 'pre-release of the same version', () {
+ var range = VersionRange(min: Version.parse('2.3.4-dev.0'), max: v234);
+
+ expect(range, allows(Version.parse('2.3.4-dev.1')));
+ expect(
+ range,
+ doesNotAllow(Version.parse('2.3.3'), Version.parse('2.3.4-dev'),
+ Version.parse('2.3.4')));
+ });
+
+ test('pre-release versions of pre-release max are included', () {
+ var range = VersionRange(max: Version.parse('2.3.4-dev.2'));
+
+ expect(range, allows(Version.parse('2.3.4-dev.1')));
+ expect(
+ range,
+ doesNotAllow(
+ Version.parse('2.3.4-dev.2'), Version.parse('2.3.4-dev.3')));
+ });
+
+ test('version must be max or less if includeMax', () {
+ var range = VersionRange(min: v123, max: v234, includeMax: true);
+
+ expect(
+ range,
+ allows(
+ Version.parse('2.3.3'),
+ Version.parse('2.3.4'),
+ // Pre-releases of the max are allowed.
+ Version.parse('2.3.4-dev')));
+ expect(range, doesNotAllow(Version.parse('2.4.3')));
+ });
+
+ test('has no min if one was not set', () {
+ var range = VersionRange(max: v123);
+
+ expect(range, allows(Version.parse('0.0.0')));
+ expect(range, doesNotAllow(Version.parse('1.2.3')));
+ });
+
+ test('has no max if one was not set', () {
+ var range = VersionRange(min: v123);
+
+ expect(range, allows(Version.parse('1.3.3'), Version.parse('999.3.3')));
+ expect(range, doesNotAllow(Version.parse('1.2.3')));
+ });
+
+ test('allows any version if there is no min or max', () {
+ var range = VersionRange();
+
+ expect(range, allows(Version.parse('0.0.0'), Version.parse('999.99.9')));
+ });
+
+ test('allows pre-releases of the max with includeMaxPreRelease', () {
+ expect(includeMaxPreReleaseRange, allows(Version.parse('2.0.0-dev')));
+ });
+ });
+
+ group('allowsAll()', () {
+ test('allows an empty constraint', () {
+ expect(
+ VersionRange(min: v123, max: v250).allowsAll(VersionConstraint.empty),
+ isTrue);
+ });
+
+ test('allows allowed versions', () {
+ var range = VersionRange(min: v123, max: v250, includeMax: true);
+ expect(range.allowsAll(v123), isFalse);
+ expect(range.allowsAll(v124), isTrue);
+ expect(range.allowsAll(v250), isTrue);
+ expect(range.allowsAll(v300), isFalse);
+ });
+
+ test('with no min', () {
+ var range = VersionRange(max: v250);
+ expect(range.allowsAll(VersionRange(min: v080, max: v140)), isTrue);
+ expect(range.allowsAll(VersionRange(min: v080, max: v300)), isFalse);
+ expect(range.allowsAll(VersionRange(max: v140)), isTrue);
+ expect(range.allowsAll(VersionRange(max: v300)), isFalse);
+ expect(range.allowsAll(range), isTrue);
+ expect(range.allowsAll(VersionConstraint.any), isFalse);
+ });
+
+ test('with no max', () {
+ var range = VersionRange(min: v010);
+ expect(range.allowsAll(VersionRange(min: v080, max: v140)), isTrue);
+ expect(range.allowsAll(VersionRange(min: v003, max: v140)), isFalse);
+ expect(range.allowsAll(VersionRange(min: v080)), isTrue);
+ expect(range.allowsAll(VersionRange(min: v003)), isFalse);
+ expect(range.allowsAll(range), isTrue);
+ expect(range.allowsAll(VersionConstraint.any), isFalse);
+ });
+
+ test('with a min and max', () {
+ var range = VersionRange(min: v010, max: v250);
+ expect(range.allowsAll(VersionRange(min: v080, max: v140)), isTrue);
+ expect(range.allowsAll(VersionRange(min: v080, max: v300)), isFalse);
+ expect(range.allowsAll(VersionRange(min: v003, max: v140)), isFalse);
+ expect(range.allowsAll(VersionRange(min: v080)), isFalse);
+ expect(range.allowsAll(VersionRange(max: v140)), isFalse);
+ expect(range.allowsAll(range), isTrue);
+ });
+
+ test("allows a bordering range that's not more inclusive", () {
+ var exclusive = VersionRange(min: v010, max: v250);
+ var inclusive = VersionRange(
+ min: v010, includeMin: true, max: v250, includeMax: true);
+ expect(inclusive.allowsAll(exclusive), isTrue);
+ expect(inclusive.allowsAll(inclusive), isTrue);
+ expect(exclusive.allowsAll(inclusive), isFalse);
+ expect(exclusive.allowsAll(exclusive), isTrue);
+ });
+
+ test('allows unions that are completely contained', () {
+ var range = VersionRange(min: v114, max: v200);
+ expect(range.allowsAll(VersionRange(min: v123, max: v124).union(v140)),
+ isTrue);
+ expect(range.allowsAll(VersionRange(min: v010, max: v124).union(v140)),
+ isFalse);
+ expect(range.allowsAll(VersionRange(min: v123, max: v234).union(v140)),
+ isFalse);
+ });
+
+ group('pre-release versions', () {
+ test('of inclusive min are excluded', () {
+ var range = VersionRange(min: v123, includeMin: true);
+
+ expect(range.allowsAll(VersionConstraint.parse('>1.2.4-dev')), isTrue);
+ expect(range.allowsAll(VersionConstraint.parse('>1.2.3-dev')), isFalse);
+ });
+
+ test('of non-pre-release max are excluded', () {
+ var range = VersionRange(max: v234);
+
+ expect(range.allowsAll(VersionConstraint.parse('<2.3.3')), isTrue);
+ expect(range.allowsAll(VersionConstraint.parse('<2.3.4-dev')), isFalse);
+ });
+
+ test('of non-pre-release max are included with includeMaxPreRelease', () {
+ expect(
+ includeMaxPreReleaseRange
+ .allowsAll(VersionConstraint.parse('<2.0.0-dev')),
+ isTrue);
+ });
+
+ test(
+ 'of non-pre-release max are included if min is a pre-release of the '
+ 'same version', () {
+ var range = VersionRange(min: Version.parse('2.3.4-dev.0'), max: v234);
+
+ expect(
+ range.allowsAll(
+ VersionConstraint.parse('>2.3.4-dev.0 <2.3.4-dev.1')),
+ isTrue);
+ });
+
+ test('of pre-release max are included', () {
+ var range = VersionRange(max: Version.parse('2.3.4-dev.2'));
+
+ expect(
+ range.allowsAll(VersionConstraint.parse('<2.3.4-dev.1')), isTrue);
+ expect(
+ range.allowsAll(VersionConstraint.parse('<2.3.4-dev.2')), isTrue);
+ expect(
+ range.allowsAll(VersionConstraint.parse('<=2.3.4-dev.2')), isFalse);
+ expect(
+ range.allowsAll(VersionConstraint.parse('<2.3.4-dev.3')), isFalse);
+ });
+ });
+ });
+
+ group('allowsAny()', () {
+ test('disallows an empty constraint', () {
+ expect(
+ VersionRange(min: v123, max: v250).allowsAny(VersionConstraint.empty),
+ isFalse);
+ });
+
+ test('allows allowed versions', () {
+ var range = VersionRange(min: v123, max: v250, includeMax: true);
+ expect(range.allowsAny(v123), isFalse);
+ expect(range.allowsAny(v124), isTrue);
+ expect(range.allowsAny(v250), isTrue);
+ expect(range.allowsAny(v300), isFalse);
+ });
+
+ test('with no min', () {
+ var range = VersionRange(max: v200);
+ expect(range.allowsAny(VersionRange(min: v140, max: v300)), isTrue);
+ expect(range.allowsAny(VersionRange(min: v234, max: v300)), isFalse);
+ expect(range.allowsAny(VersionRange(min: v140)), isTrue);
+ expect(range.allowsAny(VersionRange(min: v234)), isFalse);
+ expect(range.allowsAny(range), isTrue);
+ });
+
+ test('with no max', () {
+ var range = VersionRange(min: v072);
+ expect(range.allowsAny(VersionRange(min: v003, max: v140)), isTrue);
+ expect(range.allowsAny(VersionRange(min: v003, max: v010)), isFalse);
+ expect(range.allowsAny(VersionRange(max: v080)), isTrue);
+ expect(range.allowsAny(VersionRange(max: v003)), isFalse);
+ expect(range.allowsAny(range), isTrue);
+ });
+
+ test('with a min and max', () {
+ var range = VersionRange(min: v072, max: v200);
+ expect(range.allowsAny(VersionRange(min: v003, max: v140)), isTrue);
+ expect(range.allowsAny(VersionRange(min: v140, max: v300)), isTrue);
+ expect(range.allowsAny(VersionRange(min: v003, max: v010)), isFalse);
+ expect(range.allowsAny(VersionRange(min: v234, max: v300)), isFalse);
+ expect(range.allowsAny(VersionRange(max: v010)), isFalse);
+ expect(range.allowsAny(VersionRange(min: v234)), isFalse);
+ expect(range.allowsAny(range), isTrue);
+ });
+
+ test('allows a bordering range when both are inclusive', () {
+ expect(
+ VersionRange(max: v250).allowsAny(VersionRange(min: v250)), isFalse);
+
+ expect(
+ VersionRange(max: v250, includeMax: true)
+ .allowsAny(VersionRange(min: v250)),
+ isFalse);
+
+ expect(
+ VersionRange(max: v250)
+ .allowsAny(VersionRange(min: v250, includeMin: true)),
+ isFalse);
+
+ expect(
+ VersionRange(max: v250, includeMax: true)
+ .allowsAny(VersionRange(min: v250, includeMin: true)),
+ isTrue);
+
+ expect(
+ VersionRange(min: v250).allowsAny(VersionRange(max: v250)), isFalse);
+
+ expect(
+ VersionRange(min: v250, includeMin: true)
+ .allowsAny(VersionRange(max: v250)),
+ isFalse);
+
+ expect(
+ VersionRange(min: v250)
+ .allowsAny(VersionRange(max: v250, includeMax: true)),
+ isFalse);
+
+ expect(
+ VersionRange(min: v250, includeMin: true)
+ .allowsAny(VersionRange(max: v250, includeMax: true)),
+ isTrue);
+ });
+
+ test('allows unions that are partially contained', () {
+ var range = VersionRange(min: v114, max: v200);
+ expect(range.allowsAny(VersionRange(min: v010, max: v080).union(v140)),
+ isTrue);
+ expect(range.allowsAny(VersionRange(min: v123, max: v234).union(v300)),
+ isTrue);
+ expect(range.allowsAny(VersionRange(min: v234, max: v300).union(v010)),
+ isFalse);
+ });
+
+ group('pre-release versions', () {
+ test('of inclusive min are excluded', () {
+ var range = VersionRange(min: v123, includeMin: true);
+
+ expect(range.allowsAny(VersionConstraint.parse('<1.2.4-dev')), isTrue);
+ expect(range.allowsAny(VersionConstraint.parse('<1.2.3-dev')), isFalse);
+ });
+
+ test('of non-pre-release max are excluded', () {
+ var range = VersionRange(max: v234);
+
+ expect(range.allowsAny(VersionConstraint.parse('>2.3.3')), isTrue);
+ expect(range.allowsAny(VersionConstraint.parse('>2.3.4-dev')), isFalse);
+ });
+
+ test('of non-pre-release max are included with includeMaxPreRelease', () {
+ expect(
+ includeMaxPreReleaseRange
+ .allowsAny(VersionConstraint.parse('>2.0.0-dev')),
+ isTrue);
+ });
+
+ test(
+ 'of non-pre-release max are included if min is a pre-release of the '
+ 'same version', () {
+ var range = VersionRange(min: Version.parse('2.3.4-dev.0'), max: v234);
+
+ expect(
+ range.allowsAny(VersionConstraint.parse('>2.3.4-dev.1')), isTrue);
+ expect(range.allowsAny(VersionConstraint.parse('>2.3.4')), isFalse);
+
+ expect(
+ range.allowsAny(VersionConstraint.parse('<2.3.4-dev.1')), isTrue);
+ expect(range.allowsAny(VersionConstraint.parse('<2.3.4-dev')), isFalse);
+ });
+
+ test('of pre-release max are included', () {
+ var range = VersionConstraint.parse('<2.3.4-dev.2');
+
+ expect(
+ range.allowsAny(VersionConstraint.parse('>2.3.4-dev.1')), isTrue);
+ expect(
+ range.allowsAny(VersionConstraint.parse('>2.3.4-dev.2')), isFalse);
+ expect(
+ range.allowsAny(VersionConstraint.parse('>2.3.4-dev.3')), isFalse);
+ });
+ });
+ });
+
+ group('intersect()', () {
+ test('two overlapping ranges', () {
+ expect(
+ VersionRange(min: v123, max: v250)
+ .intersect(VersionRange(min: v200, max: v300)),
+ equals(VersionRange(min: v200, max: v250)));
+ });
+
+ test('a non-overlapping range allows no versions', () {
+ var a = VersionRange(min: v114, max: v124);
+ var b = VersionRange(min: v200, max: v250);
+ expect(a.intersect(b).isEmpty, isTrue);
+ });
+
+ test('adjacent ranges allow no versions if exclusive', () {
+ var a = VersionRange(min: v114, max: v124);
+ var b = VersionRange(min: v124, max: v200);
+ expect(a.intersect(b).isEmpty, isTrue);
+ });
+
+ test('adjacent ranges allow version if inclusive', () {
+ var a = VersionRange(min: v114, max: v124, includeMax: true);
+ var b = VersionRange(min: v124, max: v200, includeMin: true);
+ expect(a.intersect(b), equals(v124));
+ });
+
+ test('with an open range', () {
+ var open = VersionRange();
+ var a = VersionRange(min: v114, max: v124);
+ expect(open.intersect(open), equals(open));
+ expect(a.intersect(open), equals(a));
+ });
+
+ test('returns the version if the range allows it', () {
+ expect(VersionRange(min: v114, max: v124).intersect(v123), equals(v123));
+ expect(
+ VersionRange(min: v123, max: v124).intersect(v114).isEmpty, isTrue);
+ });
+
+ test('with a range with a pre-release min, returns an empty constraint',
+ () {
+ expect(
+ VersionRange(max: v200)
+ .intersect(VersionConstraint.parse('>=2.0.0-dev')),
+ equals(VersionConstraint.empty));
+ });
+
+ test('with a range with a pre-release max, returns the original', () {
+ expect(
+ VersionRange(max: v200)
+ .intersect(VersionConstraint.parse('<2.0.0-dev')),
+ equals(VersionRange(max: v200)));
+ });
+
+ group('with includeMaxPreRelease', () {
+ test('preserves includeMaxPreRelease if the max version is included', () {
+ expect(
+ includeMaxPreReleaseRange
+ .intersect(VersionConstraint.parse('<1.0.0')),
+ equals(VersionConstraint.parse('<1.0.0')));
+ expect(
+ includeMaxPreReleaseRange
+ .intersect(VersionConstraint.parse('<2.0.0')),
+ equals(VersionConstraint.parse('<2.0.0')));
+ expect(includeMaxPreReleaseRange.intersect(includeMaxPreReleaseRange),
+ equals(includeMaxPreReleaseRange));
+ expect(
+ includeMaxPreReleaseRange
+ .intersect(VersionConstraint.parse('<3.0.0')),
+ equals(includeMaxPreReleaseRange));
+ expect(
+ includeMaxPreReleaseRange
+ .intersect(VersionConstraint.parse('>1.1.4')),
+ equals(VersionRange(
+ min: v114, max: v200, alwaysIncludeMaxPreRelease: true)));
+ });
+
+ test(
+ 'and a range with a pre-release min, returns '
+ 'an intersection', () {
+ expect(
+ includeMaxPreReleaseRange
+ .intersect(VersionConstraint.parse('>=2.0.0-dev')),
+ equals(VersionConstraint.parse('>=2.0.0-dev <2.0.0')));
+ });
+
+ test(
+ 'and a range with a pre-release max, returns '
+ 'the narrower constraint', () {
+ expect(
+ includeMaxPreReleaseRange
+ .intersect(VersionConstraint.parse('<2.0.0-dev')),
+ equals(VersionConstraint.parse('<2.0.0-dev')));
+ });
+ });
+ });
+
+ group('union()', () {
+ test('with a version returns the range if it contains the version', () {
+ var range = VersionRange(min: v114, max: v124);
+ expect(range.union(v123), equals(range));
+ });
+
+ test('with a version on the edge of the range, expands the range', () {
+ expect(
+ VersionRange(min: v114, max: v124, alwaysIncludeMaxPreRelease: true)
+ .union(v124),
+ equals(VersionRange(min: v114, max: v124, includeMax: true)));
+ expect(VersionRange(min: v114, max: v124).union(v114),
+ equals(VersionRange(min: v114, max: v124, includeMin: true)));
+ });
+
+ test(
+ 'with a version allows both the range and the version if the range '
+ "doesn't contain the version", () {
+ var result = VersionRange(min: v003, max: v114).union(v124);
+ expect(result, allows(v010));
+ expect(result, doesNotAllow(v123));
+ expect(result, allows(v124));
+ });
+
+ test('returns a VersionUnion for a disjoint range', () {
+ var result = VersionRange(min: v003, max: v114)
+ .union(VersionRange(min: v130, max: v200));
+ expect(result, allows(v080));
+ expect(result, doesNotAllow(v123));
+ expect(result, allows(v140));
+ });
+
+ test('returns a VersionUnion for a disjoint range with infinite end', () {
+ void isVersionUnion(VersionConstraint constraint) {
+ expect(constraint, allows(v080));
+ expect(constraint, doesNotAllow(v123));
+ expect(constraint, allows(v140));
+ }
+
+ for (final includeAMin in [true, false]) {
+ for (final includeAMax in [true, false]) {
+ for (final includeBMin in [true, false]) {
+ for (final includeBMax in [true, false]) {
+ final a = VersionRange(
+ min: v130, includeMin: includeAMin, includeMax: includeAMax);
+ final b = VersionRange(
+ max: v114, includeMin: includeBMin, includeMax: includeBMax);
+ isVersionUnion(a.union(b));
+ isVersionUnion(b.union(a));
+ }
+ }
+ }
+ }
+ });
+
+ test('considers open ranges disjoint', () {
+ var result = VersionRange(min: v003, max: v114)
+ .union(VersionRange(min: v114, max: v200));
+ expect(result, allows(v080));
+ expect(result, doesNotAllow(v114));
+ expect(result, allows(v140));
+
+ result = VersionRange(min: v114, max: v200)
+ .union(VersionRange(min: v003, max: v114));
+ expect(result, allows(v080));
+ expect(result, doesNotAllow(v114));
+ expect(result, allows(v140));
+ });
+
+ test('returns a merged range for an overlapping range', () {
+ var result = VersionRange(min: v003, max: v114)
+ .union(VersionRange(min: v080, max: v200));
+ expect(result, equals(VersionRange(min: v003, max: v200)));
+ });
+
+ test('considers closed ranges overlapping', () {
+ var result = VersionRange(min: v003, max: v114, includeMax: true)
+ .union(VersionRange(min: v114, max: v200));
+ expect(result, equals(VersionRange(min: v003, max: v200)));
+
+ result =
+ VersionRange(min: v003, max: v114, alwaysIncludeMaxPreRelease: true)
+ .union(VersionRange(min: v114, max: v200, includeMin: true));
+ expect(result, equals(VersionRange(min: v003, max: v200)));
+
+ result = VersionRange(min: v114, max: v200)
+ .union(VersionRange(min: v003, max: v114, includeMax: true));
+ expect(result, equals(VersionRange(min: v003, max: v200)));
+
+ result = VersionRange(min: v114, max: v200, includeMin: true).union(
+ VersionRange(min: v003, max: v114, alwaysIncludeMaxPreRelease: true));
+ expect(result, equals(VersionRange(min: v003, max: v200)));
+ });
+
+ test('includes edges if either range does', () {
+ var result = VersionRange(min: v003, max: v114, includeMin: true)
+ .union(VersionRange(min: v003, max: v114, includeMax: true));
+ expect(
+ result,
+ equals(VersionRange(
+ min: v003, max: v114, includeMin: true, includeMax: true)));
+ });
+
+ test('with a range with a pre-release min, returns a constraint with a gap',
+ () {
+ var result =
+ VersionRange(max: v200).union(VersionConstraint.parse('>=2.0.0-dev'));
+ expect(result, allows(v140));
+ expect(result, doesNotAllow(Version.parse('2.0.0-alpha')));
+ expect(result, allows(Version.parse('2.0.0-dev')));
+ expect(result, allows(Version.parse('2.0.0-dev.1')));
+ expect(result, allows(Version.parse('2.0.0')));
+ });
+
+ test('with a range with a pre-release max, returns the larger constraint',
+ () {
+ expect(
+ VersionRange(max: v200).union(VersionConstraint.parse('<2.0.0-dev')),
+ equals(VersionConstraint.parse('<2.0.0-dev')));
+ });
+
+ group('with includeMaxPreRelease', () {
+ test('adds includeMaxPreRelease if the max version is included', () {
+ expect(
+ includeMaxPreReleaseRange.union(VersionConstraint.parse('<1.0.0')),
+ equals(includeMaxPreReleaseRange));
+ expect(includeMaxPreReleaseRange.union(includeMaxPreReleaseRange),
+ equals(includeMaxPreReleaseRange));
+ expect(
+ includeMaxPreReleaseRange.union(VersionConstraint.parse('<2.0.0')),
+ equals(includeMaxPreReleaseRange));
+ expect(
+ includeMaxPreReleaseRange.union(VersionConstraint.parse('<3.0.0')),
+ equals(VersionConstraint.parse('<3.0.0')));
+ });
+
+ test('and a range with a pre-release min, returns any', () {
+ expect(
+ includeMaxPreReleaseRange
+ .union(VersionConstraint.parse('>=2.0.0-dev')),
+ equals(VersionConstraint.any));
+ });
+
+ test('and a range with a pre-release max, returns the original', () {
+ expect(
+ includeMaxPreReleaseRange
+ .union(VersionConstraint.parse('<2.0.0-dev')),
+ equals(includeMaxPreReleaseRange));
+ });
+ });
+ });
+
+ group('difference()', () {
+ test('with an empty range returns the original range', () {
+ expect(
+ VersionRange(min: v003, max: v114)
+ .difference(VersionConstraint.empty),
+ equals(VersionRange(min: v003, max: v114)));
+ });
+
+ test('with a version outside the range returns the original range', () {
+ expect(VersionRange(min: v003, max: v114).difference(v200),
+ equals(VersionRange(min: v003, max: v114)));
+ });
+
+ test('with a version in the range splits the range', () {
+ expect(
+ VersionRange(min: v003, max: v114).difference(v072),
+ equals(VersionConstraint.unionOf([
+ VersionRange(
+ min: v003, max: v072, alwaysIncludeMaxPreRelease: true),
+ VersionRange(min: v072, max: v114)
+ ])));
+ });
+
+ test('with the max version makes the max exclusive', () {
+ expect(
+ VersionRange(min: v003, max: v114, includeMax: true).difference(v114),
+ equals(VersionRange(
+ min: v003, max: v114, alwaysIncludeMaxPreRelease: true)));
+ });
+
+ test('with the min version makes the min exclusive', () {
+ expect(
+ VersionRange(min: v003, max: v114, includeMin: true).difference(v003),
+ equals(VersionRange(min: v003, max: v114)));
+ });
+
+ test('with a disjoint range returns the original', () {
+ expect(
+ VersionRange(min: v003, max: v114)
+ .difference(VersionRange(min: v123, max: v140)),
+ equals(VersionRange(min: v003, max: v114)));
+ });
+
+ test('with an adjacent range returns the original', () {
+ expect(
+ VersionRange(min: v003, max: v114, includeMax: true)
+ .difference(VersionRange(min: v114, max: v140)),
+ equals(VersionRange(min: v003, max: v114, includeMax: true)));
+ });
+
+ test('with a range at the beginning cuts off the beginning of the range',
+ () {
+ expect(
+ VersionRange(min: v080, max: v130)
+ .difference(VersionRange(min: v010, max: v114)),
+ equals(VersionConstraint.parse('>=1.1.4-0 <1.3.0')));
+ expect(
+ VersionRange(min: v080, max: v130)
+ .difference(VersionRange(max: v114)),
+ equals(VersionConstraint.parse('>=1.1.4-0 <1.3.0')));
+ expect(
+ VersionRange(min: v080, max: v130)
+ .difference(VersionRange(min: v010, max: v114, includeMax: true)),
+ equals(VersionRange(min: v114, max: v130)));
+ expect(
+ VersionRange(min: v080, max: v130, includeMin: true)
+ .difference(VersionRange(min: v010, max: v080, includeMax: true)),
+ equals(VersionRange(min: v080, max: v130)));
+ expect(
+ VersionRange(min: v080, max: v130, includeMax: true)
+ .difference(VersionRange(min: v080, max: v130)),
+ equals(VersionConstraint.parse('>=1.3.0-0 <=1.3.0')));
+ });
+
+ test('with a range at the end cuts off the end of the range', () {
+ expect(
+ VersionRange(min: v080, max: v130)
+ .difference(VersionRange(min: v114, max: v140)),
+ equals(VersionRange(min: v080, max: v114, includeMax: true)));
+ expect(
+ VersionRange(min: v080, max: v130)
+ .difference(VersionRange(min: v114)),
+ equals(VersionRange(min: v080, max: v114, includeMax: true)));
+ expect(
+ VersionRange(min: v080, max: v130)
+ .difference(VersionRange(min: v114, max: v140, includeMin: true)),
+ equals(VersionRange(
+ min: v080, max: v114, alwaysIncludeMaxPreRelease: true)));
+ expect(
+ VersionRange(min: v080, max: v130, includeMax: true)
+ .difference(VersionRange(min: v130, max: v140, includeMin: true)),
+ equals(VersionRange(
+ min: v080, max: v130, alwaysIncludeMaxPreRelease: true)));
+ expect(
+ VersionRange(min: v080, max: v130, includeMin: true)
+ .difference(VersionRange(min: v080, max: v130)),
+ equals(v080));
+ });
+
+ test('with a range in the middle cuts the range in half', () {
+ expect(
+ VersionRange(min: v003, max: v130)
+ .difference(VersionRange(min: v072, max: v114)),
+ equals(VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v072, includeMax: true),
+ VersionConstraint.parse('>=1.1.4-0 <1.3.0')
+ ])));
+ });
+
+ test('with a totally covering range returns empty', () {
+ expect(
+ VersionRange(min: v114, max: v200)
+ .difference(VersionRange(min: v072, max: v300)),
+ isEmpty);
+ expect(
+ VersionRange(min: v003, max: v114)
+ .difference(VersionRange(min: v003, max: v114)),
+ isEmpty);
+ expect(
+ VersionRange(min: v003, max: v114, includeMin: true, includeMax: true)
+ .difference(VersionRange(
+ min: v003, max: v114, includeMin: true, includeMax: true)),
+ isEmpty);
+ });
+
+ test(
+ "with a version union that doesn't cover the range, returns the "
+ 'original', () {
+ expect(
+ VersionRange(min: v114, max: v140)
+ .difference(VersionConstraint.unionOf([v010, v200])),
+ equals(VersionRange(min: v114, max: v140)));
+ });
+
+ test('with a version union that intersects the ends, chops them off', () {
+ expect(
+ VersionRange(min: v114, max: v140).difference(
+ VersionConstraint.unionOf([
+ VersionRange(min: v080, max: v123),
+ VersionRange(min: v130, max: v200)
+ ])),
+ equals(VersionConstraint.parse('>=1.2.3-0 <=1.3.0')));
+ });
+
+ test('with a version union that intersects the middle, chops it up', () {
+ expect(
+ VersionRange(min: v114, max: v140)
+ .difference(VersionConstraint.unionOf([v123, v124, v130])),
+ equals(VersionConstraint.unionOf([
+ VersionRange(
+ min: v114, max: v123, alwaysIncludeMaxPreRelease: true),
+ VersionRange(
+ min: v123, max: v124, alwaysIncludeMaxPreRelease: true),
+ VersionRange(
+ min: v124, max: v130, alwaysIncludeMaxPreRelease: true),
+ VersionRange(min: v130, max: v140)
+ ])));
+ });
+
+ test('with a version union that covers the whole range, returns empty', () {
+ expect(
+ VersionRange(min: v114, max: v140).difference(
+ VersionConstraint.unionOf([v003, VersionRange(min: v010)])),
+ equals(VersionConstraint.empty));
+ });
+
+ test('with a range with a pre-release min, returns the original', () {
+ expect(
+ VersionRange(max: v200)
+ .difference(VersionConstraint.parse('>=2.0.0-dev')),
+ equals(VersionRange(max: v200)));
+ });
+
+ test('with a range with a pre-release max, returns null', () {
+ expect(
+ VersionRange(max: v200)
+ .difference(VersionConstraint.parse('<2.0.0-dev')),
+ equals(VersionConstraint.empty));
+ });
+
+ group('with includeMaxPreRelease', () {
+ group('for the minuend', () {
+ test('preserves includeMaxPreRelease if the max version is included',
+ () {
+ expect(
+ includeMaxPreReleaseRange
+ .difference(VersionConstraint.parse('<1.0.0')),
+ equals(VersionRange(
+ min: Version.parse('1.0.0-0'),
+ max: v200,
+ includeMin: true,
+ alwaysIncludeMaxPreRelease: true)));
+ expect(
+ includeMaxPreReleaseRange
+ .difference(VersionConstraint.parse('<2.0.0')),
+ equals(VersionRange(
+ min: v200.firstPreRelease,
+ max: v200,
+ includeMin: true,
+ alwaysIncludeMaxPreRelease: true)));
+ expect(
+ includeMaxPreReleaseRange.difference(includeMaxPreReleaseRange),
+ equals(VersionConstraint.empty));
+ expect(
+ includeMaxPreReleaseRange
+ .difference(VersionConstraint.parse('<3.0.0')),
+ equals(VersionConstraint.empty));
+ });
+
+ test('with a range with a pre-release min, adjusts the max', () {
+ expect(
+ includeMaxPreReleaseRange
+ .difference(VersionConstraint.parse('>=2.0.0-dev')),
+ equals(VersionConstraint.parse('<2.0.0-dev')));
+ });
+
+ test('with a range with a pre-release max, adjusts the min', () {
+ expect(
+ includeMaxPreReleaseRange
+ .difference(VersionConstraint.parse('<2.0.0-dev')),
+ equals(VersionConstraint.parse('>=2.0.0-dev <2.0.0')));
+ });
+ });
+
+ group('for the subtrahend', () {
+ group("doesn't create a pre-release minimum", () {
+ test('when cutting off the bottom', () {
+ expect(
+ VersionConstraint.parse('<3.0.0')
+ .difference(includeMaxPreReleaseRange),
+ equals(VersionRange(min: v200, max: v300, includeMin: true)));
+ });
+
+ test('with splitting down the middle', () {
+ expect(
+ VersionConstraint.parse('<4.0.0').difference(VersionRange(
+ min: v200,
+ max: v300,
+ includeMin: true,
+ alwaysIncludeMaxPreRelease: true)),
+ equals(VersionConstraint.unionOf([
+ VersionRange(max: v200, alwaysIncludeMaxPreRelease: true),
+ VersionConstraint.parse('>=3.0.0 <4.0.0')
+ ])));
+ });
+
+ test('can leave a single version', () {
+ expect(
+ VersionConstraint.parse('<=2.0.0')
+ .difference(includeMaxPreReleaseRange),
+ equals(v200));
+ });
+ });
+ });
+ });
+ });
+
+ test('isEmpty', () {
+ expect(VersionRange().isEmpty, isFalse);
+ expect(VersionRange(min: v123, max: v124).isEmpty, isFalse);
+ });
+
+ group('compareTo()', () {
+ test('orders by minimum first', () {
+ _expectComparesSmaller(VersionRange(min: v003, max: v080),
+ VersionRange(min: v010, max: v072));
+ _expectComparesSmaller(VersionRange(min: v003, max: v080),
+ VersionRange(min: v010, max: v080));
+ _expectComparesSmaller(VersionRange(min: v003, max: v080),
+ VersionRange(min: v010, max: v114));
+ });
+
+ test('orders by maximum second', () {
+ _expectComparesSmaller(VersionRange(min: v003, max: v010),
+ VersionRange(min: v003, max: v072));
+ });
+
+ test('includeMin comes before !includeMin', () {
+ _expectComparesSmaller(
+ VersionRange(min: v003, max: v080, includeMin: true),
+ VersionRange(min: v003, max: v080));
+ });
+
+ test('includeMax comes after !includeMax', () {
+ _expectComparesSmaller(VersionRange(min: v003, max: v080),
+ VersionRange(min: v003, max: v080, includeMax: true));
+ });
+
+ test('includeMaxPreRelease comes after !includeMaxPreRelease', () {
+ _expectComparesSmaller(
+ VersionRange(max: v200), includeMaxPreReleaseRange);
+ });
+
+ test('no minimum comes before small minimum', () {
+ _expectComparesSmaller(
+ VersionRange(max: v010), VersionRange(min: v003, max: v010));
+ _expectComparesSmaller(VersionRange(max: v010, includeMin: true),
+ VersionRange(min: v003, max: v010));
+ });
+
+ test('no maximium comes after large maximum', () {
+ _expectComparesSmaller(
+ VersionRange(min: v003, max: v300), VersionRange(min: v003));
+ _expectComparesSmaller(VersionRange(min: v003, max: v300),
+ VersionRange(min: v003, includeMax: true));
+ });
+ });
+}
+
+void _expectComparesSmaller(VersionRange smaller, VersionRange larger) {
+ expect(smaller.compareTo(larger), lessThan(0),
+ reason: 'expected $smaller to sort below $larger');
+ expect(larger.compareTo(smaller), greaterThan(0),
+ reason: 'expected $larger to sort above $smaller');
+}
diff --git a/pkgs/pub_semver/test/version_test.dart b/pkgs/pub_semver/test/version_test.dart
new file mode 100644
index 0000000..d7f1197
--- /dev/null
+++ b/pkgs/pub_semver/test/version_test.dart
@@ -0,0 +1,411 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ test('none', () {
+ expect(Version.none.toString(), equals('0.0.0'));
+ });
+
+ test('prioritize()', () {
+ // A correctly sorted list of versions in order of increasing priority.
+ var versions = [
+ '1.0.0-alpha',
+ '2.0.0-alpha',
+ '1.0.0',
+ '1.0.0+build',
+ '1.0.1',
+ '1.1.0',
+ '2.0.0'
+ ];
+
+ // Ensure that every pair of versions is prioritized in the order that it
+ // appears in the list.
+ for (var i = 0; i < versions.length; i++) {
+ for (var j = 0; j < versions.length; j++) {
+ var a = Version.parse(versions[i]);
+ var b = Version.parse(versions[j]);
+ expect(Version.prioritize(a, b), equals(i.compareTo(j)));
+ }
+ }
+ });
+
+ test('antiprioritize()', () {
+ // A correctly sorted list of versions in order of increasing antipriority.
+ var versions = [
+ '2.0.0-alpha',
+ '1.0.0-alpha',
+ '2.0.0',
+ '1.1.0',
+ '1.0.1',
+ '1.0.0+build',
+ '1.0.0'
+ ];
+
+ // Ensure that every pair of versions is prioritized in the order that it
+ // appears in the list.
+ for (var i = 0; i < versions.length; i++) {
+ for (var j = 0; j < versions.length; j++) {
+ var a = Version.parse(versions[i]);
+ var b = Version.parse(versions[j]);
+ expect(Version.antiprioritize(a, b), equals(i.compareTo(j)));
+ }
+ }
+ });
+
+ group('constructor', () {
+ test('throws on negative numbers', () {
+ expect(() => Version(-1, 1, 1), throwsArgumentError);
+ expect(() => Version(1, -1, 1), throwsArgumentError);
+ expect(() => Version(1, 1, -1), throwsArgumentError);
+ });
+ });
+
+ group('comparison', () {
+ // A correctly sorted list of versions.
+ var versions = [
+ '1.0.0-alpha',
+ '1.0.0-alpha.1',
+ '1.0.0-beta.2',
+ '1.0.0-beta.11',
+ '1.0.0-rc.1',
+ '1.0.0-rc.1+build.1',
+ '1.0.0',
+ '1.0.0+0.3.7',
+ '1.3.7+build',
+ '1.3.7+build.2.b8f12d7',
+ '1.3.7+build.11.e0f985a',
+ '2.0.0',
+ '2.1.0',
+ '2.2.0',
+ '2.11.0',
+ '2.11.1'
+ ];
+
+ test('compareTo()', () {
+ // Ensure that every pair of versions compares in the order that it
+ // appears in the list.
+ for (var i = 0; i < versions.length; i++) {
+ for (var j = 0; j < versions.length; j++) {
+ var a = Version.parse(versions[i]);
+ var b = Version.parse(versions[j]);
+ expect(a.compareTo(b), equals(i.compareTo(j)));
+ }
+ }
+ });
+
+ test('operators', () {
+ for (var i = 0; i < versions.length; i++) {
+ for (var j = 0; j < versions.length; j++) {
+ var a = Version.parse(versions[i]);
+ var b = Version.parse(versions[j]);
+ expect(a < b, equals(i < j));
+ expect(a > b, equals(i > j));
+ expect(a <= b, equals(i <= j));
+ expect(a >= b, equals(i >= j));
+ expect(a == b, equals(i == j));
+ expect(a != b, equals(i != j));
+ }
+ }
+ });
+
+ test('equality', () {
+ expect(Version.parse('01.2.3'), equals(Version.parse('1.2.3')));
+ expect(Version.parse('1.02.3'), equals(Version.parse('1.2.3')));
+ expect(Version.parse('1.2.03'), equals(Version.parse('1.2.3')));
+ expect(Version.parse('1.2.3-01'), equals(Version.parse('1.2.3-1')));
+ expect(Version.parse('1.2.3+01'), equals(Version.parse('1.2.3+1')));
+ });
+ });
+
+ test('allows()', () {
+ expect(v123, allows(v123));
+ expect(
+ v123,
+ doesNotAllow(
+ Version.parse('2.2.3'),
+ Version.parse('1.3.3'),
+ Version.parse('1.2.4'),
+ Version.parse('1.2.3-dev'),
+ Version.parse('1.2.3+build')));
+ });
+
+ test('allowsAll()', () {
+ expect(v123.allowsAll(v123), isTrue);
+ expect(v123.allowsAll(v003), isFalse);
+ expect(v123.allowsAll(VersionRange(min: v114, max: v124)), isFalse);
+ expect(v123.allowsAll(VersionConstraint.any), isFalse);
+ expect(v123.allowsAll(VersionConstraint.empty), isTrue);
+ });
+
+ test('allowsAny()', () {
+ expect(v123.allowsAny(v123), isTrue);
+ expect(v123.allowsAny(v003), isFalse);
+ expect(v123.allowsAny(VersionRange(min: v114, max: v124)), isTrue);
+ expect(v123.allowsAny(VersionConstraint.any), isTrue);
+ expect(v123.allowsAny(VersionConstraint.empty), isFalse);
+ });
+
+ test('intersect()', () {
+ // Intersecting the same version returns the version.
+ expect(v123.intersect(v123), equals(v123));
+
+ // Intersecting a different version allows no versions.
+ expect(v123.intersect(v114).isEmpty, isTrue);
+
+ // Intersecting a range returns the version if the range allows it.
+ expect(v123.intersect(VersionRange(min: v114, max: v124)), equals(v123));
+
+ // Intersecting a range allows no versions if the range doesn't allow it.
+ expect(v114.intersect(VersionRange(min: v123, max: v124)).isEmpty, isTrue);
+ });
+
+ group('union()', () {
+ test('with the same version returns the version', () {
+ expect(v123.union(v123), equals(v123));
+ });
+
+ test('with a different version returns a version that matches both', () {
+ var result = v123.union(v080);
+ expect(result, allows(v123));
+ expect(result, allows(v080));
+
+ // Nothing in between should match.
+ expect(result, doesNotAllow(v114));
+ });
+
+ test('with a range returns the range if it contains the version', () {
+ var range = VersionRange(min: v114, max: v124);
+ expect(v123.union(range), equals(range));
+ });
+
+ test('with a range with the version on the edge, expands the range', () {
+ expect(
+ v124.union(VersionRange(
+ min: v114, max: v124, alwaysIncludeMaxPreRelease: true)),
+ equals(VersionRange(min: v114, max: v124, includeMax: true)));
+ expect(
+ v124.firstPreRelease.union(VersionRange(min: v114, max: v124)),
+ equals(VersionRange(
+ min: v114, max: v124.firstPreRelease, includeMax: true)));
+ expect(v114.union(VersionRange(min: v114, max: v124)),
+ equals(VersionRange(min: v114, max: v124, includeMin: true)));
+ });
+
+ test(
+ 'with a range allows both the range and the version if the range '
+ "doesn't contain the version", () {
+ var result = v123.union(VersionRange(min: v003, max: v114));
+ expect(result, allows(v123));
+ expect(result, allows(v010));
+ });
+ });
+
+ group('difference()', () {
+ test('with the same version returns an empty constraint', () {
+ expect(v123.difference(v123), isEmpty);
+ });
+
+ test('with a different version returns the original version', () {
+ expect(v123.difference(v080), equals(v123));
+ });
+
+ test('returns an empty constraint with a range that contains the version',
+ () {
+ expect(v123.difference(VersionRange(min: v114, max: v124)), isEmpty);
+ });
+
+ test("returns the version constraint with a range that doesn't contain it",
+ () {
+ expect(v123.difference(VersionRange(min: v140, max: v300)), equals(v123));
+ });
+ });
+
+ test('isEmpty', () {
+ expect(v123.isEmpty, isFalse);
+ });
+
+ test('nextMajor', () {
+ expect(v123.nextMajor, equals(v200));
+ expect(v114.nextMajor, equals(v200));
+ expect(v200.nextMajor, equals(v300));
+
+ // Ignores pre-release if not on a major version.
+ expect(Version.parse('1.2.3-dev').nextMajor, equals(v200));
+
+ // Just removes it if on a major version.
+ expect(Version.parse('2.0.0-dev').nextMajor, equals(v200));
+
+ // Strips build suffix.
+ expect(Version.parse('1.2.3+patch').nextMajor, equals(v200));
+ });
+
+ test('nextMinor', () {
+ expect(v123.nextMinor, equals(v130));
+ expect(v130.nextMinor, equals(v140));
+
+ // Ignores pre-release if not on a minor version.
+ expect(Version.parse('1.2.3-dev').nextMinor, equals(v130));
+
+ // Just removes it if on a minor version.
+ expect(Version.parse('1.3.0-dev').nextMinor, equals(v130));
+
+ // Strips build suffix.
+ expect(Version.parse('1.2.3+patch').nextMinor, equals(v130));
+ });
+
+ test('nextPatch', () {
+ expect(v123.nextPatch, equals(v124));
+ expect(v200.nextPatch, equals(v201));
+
+ // Just removes pre-release version if present.
+ expect(Version.parse('1.2.4-dev').nextPatch, equals(v124));
+
+ // Strips build suffix.
+ expect(Version.parse('1.2.3+patch').nextPatch, equals(v124));
+ });
+
+ test('nextBreaking', () {
+ expect(v123.nextBreaking, equals(v200));
+ expect(v072.nextBreaking, equals(v080));
+ expect(v003.nextBreaking, equals(v010));
+
+ // Removes pre-release version if present.
+ expect(Version.parse('1.2.3-dev').nextBreaking, equals(v200));
+
+ // Strips build suffix.
+ expect(Version.parse('1.2.3+patch').nextBreaking, equals(v200));
+ });
+
+ test('parse()', () {
+ expect(Version.parse('0.0.0'), equals(Version(0, 0, 0)));
+ expect(Version.parse('12.34.56'), equals(Version(12, 34, 56)));
+
+ expect(Version.parse('1.2.3-alpha.1'),
+ equals(Version(1, 2, 3, pre: 'alpha.1')));
+ expect(Version.parse('1.2.3-x.7.z-92'),
+ equals(Version(1, 2, 3, pre: 'x.7.z-92')));
+
+ expect(Version.parse('1.2.3+build.1'),
+ equals(Version(1, 2, 3, build: 'build.1')));
+ expect(Version.parse('1.2.3+x.7.z-92'),
+ equals(Version(1, 2, 3, build: 'x.7.z-92')));
+
+ expect(Version.parse('1.0.0-rc-1+build-1'),
+ equals(Version(1, 0, 0, pre: 'rc-1', build: 'build-1')));
+
+ expect(() => Version.parse('1.0'), throwsFormatException);
+ expect(() => Version.parse('1a2b3'), throwsFormatException);
+ expect(() => Version.parse('1.2.3.4'), throwsFormatException);
+ expect(() => Version.parse('1234'), throwsFormatException);
+ expect(() => Version.parse('-2.3.4'), throwsFormatException);
+ expect(() => Version.parse('1.3-pre'), throwsFormatException);
+ expect(() => Version.parse('1.3+build'), throwsFormatException);
+ expect(() => Version.parse('1.3+bu?!3ild'), throwsFormatException);
+ });
+
+ group('toString()', () {
+ test('returns the version string', () {
+ expect(Version(0, 0, 0).toString(), equals('0.0.0'));
+ expect(Version(12, 34, 56).toString(), equals('12.34.56'));
+
+ expect(
+ Version(1, 2, 3, pre: 'alpha.1').toString(), equals('1.2.3-alpha.1'));
+ expect(Version(1, 2, 3, pre: 'x.7.z-92').toString(),
+ equals('1.2.3-x.7.z-92'));
+
+ expect(Version(1, 2, 3, build: 'build.1').toString(),
+ equals('1.2.3+build.1'));
+ expect(Version(1, 2, 3, pre: 'pre', build: 'bui').toString(),
+ equals('1.2.3-pre+bui'));
+ });
+
+ test('preserves leading zeroes', () {
+ expect(Version.parse('001.02.0003-01.dev+pre.002').toString(),
+ equals('001.02.0003-01.dev+pre.002'));
+ });
+ });
+
+ group('canonicalizedVersion', () {
+ test('returns version string', () {
+ expect(Version(0, 0, 0).canonicalizedVersion, equals('0.0.0'));
+ expect(Version(12, 34, 56).canonicalizedVersion, equals('12.34.56'));
+
+ expect(Version(1, 2, 3, pre: 'alpha.1').canonicalizedVersion,
+ equals('1.2.3-alpha.1'));
+ expect(Version(1, 2, 3, pre: 'x.7.z-92').canonicalizedVersion,
+ equals('1.2.3-x.7.z-92'));
+
+ expect(Version(1, 2, 3, build: 'build.1').canonicalizedVersion,
+ equals('1.2.3+build.1'));
+ expect(Version(1, 2, 3, pre: 'pre', build: 'bui').canonicalizedVersion,
+ equals('1.2.3-pre+bui'));
+ });
+
+ test('discards leading zeroes', () {
+ expect(Version.parse('001.02.0003-01.dev+pre.002').canonicalizedVersion,
+ equals('1.2.3-1.dev+pre.2'));
+ });
+
+ test('example from documentation', () {
+ final v = Version.parse('01.02.03-01.dev+pre.02');
+
+ assert(v.toString() == '01.02.03-01.dev+pre.02');
+ assert(v.canonicalizedVersion == '1.2.3-1.dev+pre.2');
+ assert(Version.parse(v.canonicalizedVersion) == v);
+ });
+ });
+
+ group('primary', () {
+ test('single', () {
+ expect(
+ _primary([
+ '1.2.3',
+ ]).toString(),
+ '1.2.3',
+ );
+ });
+
+ test('normal', () {
+ expect(
+ _primary([
+ '1.2.3',
+ '1.2.2',
+ ]).toString(),
+ '1.2.3',
+ );
+ });
+
+ test('all prerelease', () {
+ expect(
+ _primary([
+ '1.2.2-dev.1',
+ '1.2.2-dev.2',
+ ]).toString(),
+ '1.2.2-dev.2',
+ );
+ });
+
+ test('later prerelease', () {
+ expect(
+ _primary([
+ '1.2.3',
+ '1.2.3-dev',
+ ]).toString(),
+ '1.2.3',
+ );
+ });
+
+ test('empty', () {
+ expect(() => Version.primary([]), throwsStateError);
+ });
+ });
+}
+
+Version _primary(List<String> input) =>
+ Version.primary(input.map(Version.parse).toList());
diff --git a/pkgs/pub_semver/test/version_union_test.dart b/pkgs/pub_semver/test/version_union_test.dart
new file mode 100644
index 0000000..857f10e
--- /dev/null
+++ b/pkgs/pub_semver/test/version_union_test.dart
@@ -0,0 +1,482 @@
+// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ group('factory', () {
+ test('ignores empty constraints', () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionConstraint.empty,
+ VersionConstraint.empty,
+ v123,
+ VersionConstraint.empty
+ ]),
+ equals(v123));
+
+ expect(
+ VersionConstraint.unionOf(
+ [VersionConstraint.empty, VersionConstraint.empty]),
+ isEmpty);
+ });
+
+ test('returns an empty constraint for an empty list', () {
+ expect(VersionConstraint.unionOf([]), isEmpty);
+ });
+
+ test('any constraints override everything', () {
+ expect(
+ VersionConstraint.unionOf([
+ v123,
+ VersionConstraint.any,
+ v200,
+ VersionRange(min: v234, max: v250)
+ ]),
+ equals(VersionConstraint.any));
+ });
+
+ test('flattens other unions', () {
+ expect(
+ VersionConstraint.unionOf([
+ v072,
+ VersionConstraint.unionOf([v123, v124]),
+ v250
+ ]),
+ equals(VersionConstraint.unionOf([v072, v123, v124, v250])));
+ });
+
+ test('returns a single merged range as-is', () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v080, max: v140),
+ VersionRange(min: v123, max: v200)
+ ]),
+ equals(VersionRange(min: v080, max: v200)));
+ });
+ });
+
+ group('equality', () {
+ test("doesn't depend on original order", () {
+ expect(
+ VersionConstraint.unionOf([
+ v250,
+ VersionRange(min: v201, max: v234),
+ v124,
+ v072,
+ VersionRange(min: v080, max: v114),
+ v123
+ ]),
+ equals(VersionConstraint.unionOf([
+ v072,
+ VersionRange(min: v080, max: v114),
+ v123,
+ v124,
+ VersionRange(min: v201, max: v234),
+ v250
+ ])));
+ });
+
+ test('merges overlapping ranges', () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v072),
+ VersionRange(min: v010, max: v080),
+ VersionRange(min: v114, max: v124),
+ VersionRange(min: v123, max: v130)
+ ]),
+ equals(VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v080),
+ VersionRange(min: v114, max: v130)
+ ])));
+ });
+
+ test('merges adjacent ranges', () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v072, includeMax: true),
+ VersionRange(min: v072, max: v080),
+ VersionRange(
+ min: v114, max: v124, alwaysIncludeMaxPreRelease: true),
+ VersionRange(min: v124, max: v130, includeMin: true),
+ VersionRange(min: v130.firstPreRelease, max: v200, includeMin: true)
+ ]),
+ equals(VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v080),
+ VersionRange(min: v114, max: v200)
+ ])));
+ });
+
+ test("doesn't merge not-quite-adjacent ranges", () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v114, max: v124),
+ VersionRange(min: v124, max: v130, includeMin: true)
+ ]),
+ isNot(equals(VersionRange(min: v114, max: v130))));
+
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v072),
+ VersionRange(min: v072, max: v080)
+ ]),
+ isNot(equals(VersionRange(min: v003, max: v080))));
+ });
+
+ test('merges version numbers into ranges', () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v072),
+ v010,
+ VersionRange(min: v114, max: v124),
+ v123
+ ]),
+ equals(VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v072),
+ VersionRange(min: v114, max: v124)
+ ])));
+ });
+
+ test('merges adjacent version numbers into ranges', () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(
+ min: v003, max: v072, alwaysIncludeMaxPreRelease: true),
+ v072,
+ v114,
+ VersionRange(min: v114, max: v124),
+ v124.firstPreRelease
+ ]),
+ equals(VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v072, includeMax: true),
+ VersionRange(
+ min: v114,
+ max: v124.firstPreRelease,
+ includeMin: true,
+ includeMax: true)
+ ])));
+ });
+
+ test("doesn't merge not-quite-adjacent version numbers into ranges", () {
+ expect(
+ VersionConstraint.unionOf([VersionRange(min: v003, max: v072), v072]),
+ isNot(equals(VersionRange(min: v003, max: v072, includeMax: true))));
+ });
+ });
+
+ test('isEmpty returns false', () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v080),
+ VersionRange(min: v123, max: v130),
+ ]),
+ isNot(isEmpty));
+ });
+
+ test('isAny returns false', () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v080),
+ VersionRange(min: v123, max: v130),
+ ]).isAny,
+ isFalse);
+ });
+
+ test('allows() allows anything the components allow', () {
+ var union = VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v080),
+ VersionRange(min: v123, max: v130),
+ v200
+ ]);
+
+ expect(union, allows(v010));
+ expect(union, doesNotAllow(v080));
+ expect(union, allows(v124));
+ expect(union, doesNotAllow(v140));
+ expect(union, allows(v200));
+ });
+
+ group('allowsAll()', () {
+ test('for a version, returns true if any component allows the version', () {
+ var union = VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v080),
+ VersionRange(min: v123, max: v130),
+ v200
+ ]);
+
+ expect(union.allowsAll(v010), isTrue);
+ expect(union.allowsAll(v080), isFalse);
+ expect(union.allowsAll(v124), isTrue);
+ expect(union.allowsAll(v140), isFalse);
+ expect(union.allowsAll(v200), isTrue);
+ });
+
+ test(
+ 'for a version range, returns true if any component allows the whole '
+ 'range', () {
+ var union = VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v080),
+ VersionRange(min: v123, max: v130)
+ ]);
+
+ expect(union.allowsAll(VersionRange(min: v003, max: v080)), isTrue);
+ expect(union.allowsAll(VersionRange(min: v010, max: v072)), isTrue);
+ expect(union.allowsAll(VersionRange(min: v010, max: v124)), isFalse);
+ });
+
+ group('for a union,', () {
+ var union = VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v080),
+ VersionRange(min: v123, max: v130)
+ ]);
+
+ test('returns true if every constraint matches a different constraint',
+ () {
+ expect(
+ union.allowsAll(VersionConstraint.unionOf([
+ VersionRange(min: v010, max: v072),
+ VersionRange(min: v124, max: v130)
+ ])),
+ isTrue);
+ });
+
+ test('returns true if every constraint matches the same constraint', () {
+ expect(
+ union.allowsAll(VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v010),
+ VersionRange(min: v072, max: v080)
+ ])),
+ isTrue);
+ });
+
+ test("returns false if there's an unmatched constraint", () {
+ expect(
+ union.allowsAll(VersionConstraint.unionOf([
+ VersionRange(min: v010, max: v072),
+ VersionRange(min: v124, max: v130),
+ VersionRange(min: v140, max: v200)
+ ])),
+ isFalse);
+ });
+
+ test("returns false if a constraint isn't fully matched", () {
+ expect(
+ union.allowsAll(VersionConstraint.unionOf([
+ VersionRange(min: v010, max: v114),
+ VersionRange(min: v124, max: v130)
+ ])),
+ isFalse);
+ });
+ });
+ });
+
+ group('allowsAny()', () {
+ test('for a version, returns true if any component allows the version', () {
+ var union = VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v080),
+ VersionRange(min: v123, max: v130),
+ v200
+ ]);
+
+ expect(union.allowsAny(v010), isTrue);
+ expect(union.allowsAny(v080), isFalse);
+ expect(union.allowsAny(v124), isTrue);
+ expect(union.allowsAny(v140), isFalse);
+ expect(union.allowsAny(v200), isTrue);
+ });
+
+ test(
+ 'for a version range, returns true if any component allows part of '
+ 'the range', () {
+ var union =
+ VersionConstraint.unionOf([VersionRange(min: v003, max: v080), v123]);
+
+ expect(union.allowsAny(VersionRange(min: v010, max: v114)), isTrue);
+ expect(union.allowsAny(VersionRange(min: v114, max: v124)), isTrue);
+ expect(union.allowsAny(VersionRange(min: v124, max: v130)), isFalse);
+ });
+
+ group('for a union,', () {
+ var union = VersionConstraint.unionOf([
+ VersionRange(min: v010, max: v080),
+ VersionRange(min: v123, max: v130)
+ ]);
+
+ test('returns true if any constraint matches', () {
+ expect(
+ union.allowsAny(VersionConstraint.unionOf(
+ [v072, VersionRange(min: v200, max: v300)])),
+ isTrue);
+
+ expect(
+ union.allowsAny(VersionConstraint.unionOf(
+ [v003, VersionRange(min: v124, max: v300)])),
+ isTrue);
+ });
+
+ test('returns false if no constraint matches', () {
+ expect(
+ union.allowsAny(VersionConstraint.unionOf([
+ v003,
+ VersionRange(min: v130, max: v140),
+ VersionRange(min: v140, max: v200)
+ ])),
+ isFalse);
+ });
+ });
+ });
+
+ group('intersect()', () {
+ test('with an overlapping version, returns that version', () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v010, max: v080),
+ VersionRange(min: v123, max: v140)
+ ]).intersect(v072),
+ equals(v072));
+ });
+
+ test('with a non-overlapping version, returns an empty constraint', () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v010, max: v080),
+ VersionRange(min: v123, max: v140)
+ ]).intersect(v300),
+ isEmpty);
+ });
+
+ test('with an overlapping range, returns that range', () {
+ var range = VersionRange(min: v072, max: v080);
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v010, max: v080),
+ VersionRange(min: v123, max: v140)
+ ]).intersect(range),
+ equals(range));
+ });
+
+ test('with a non-overlapping range, returns an empty constraint', () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v010, max: v080),
+ VersionRange(min: v123, max: v140)
+ ]).intersect(VersionRange(min: v080, max: v123)),
+ isEmpty);
+ });
+
+ test('with a parially-overlapping range, returns the overlapping parts',
+ () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v010, max: v080),
+ VersionRange(min: v123, max: v140)
+ ]).intersect(VersionRange(min: v072, max: v130)),
+ equals(VersionConstraint.unionOf([
+ VersionRange(min: v072, max: v080),
+ VersionRange(min: v123, max: v130)
+ ])));
+ });
+
+ group('for a union,', () {
+ var union = VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v080),
+ VersionRange(min: v123, max: v130)
+ ]);
+
+ test('returns the overlapping parts', () {
+ expect(
+ union.intersect(VersionConstraint.unionOf([
+ v010,
+ VersionRange(min: v072, max: v124),
+ VersionRange(min: v124, max: v130)
+ ])),
+ equals(VersionConstraint.unionOf([
+ v010,
+ VersionRange(min: v072, max: v080),
+ VersionRange(min: v123, max: v124),
+ VersionRange(min: v124, max: v130)
+ ])));
+ });
+
+ test("drops parts that don't match", () {
+ expect(
+ union.intersect(VersionConstraint.unionOf([
+ v003,
+ VersionRange(min: v072, max: v080),
+ VersionRange(min: v080, max: v123)
+ ])),
+ equals(VersionRange(min: v072, max: v080)));
+ });
+ });
+ });
+
+ group('difference()', () {
+ test("ignores ranges that don't intersect", () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v072, max: v080),
+ VersionRange(min: v123, max: v130)
+ ]).difference(VersionConstraint.unionOf([
+ VersionRange(min: v003, max: v010),
+ VersionRange(min: v080, max: v123),
+ VersionRange(min: v140)
+ ])),
+ equals(VersionConstraint.unionOf([
+ VersionRange(min: v072, max: v080),
+ VersionRange(min: v123, max: v130)
+ ])));
+ });
+
+ test('removes overlapping portions', () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v010, max: v080),
+ VersionRange(min: v123, max: v130)
+ ]).difference(VersionConstraint.unionOf(
+ [VersionRange(min: v003, max: v072), VersionRange(min: v124)])),
+ equals(VersionConstraint.unionOf([
+ VersionRange(
+ min: v072.firstPreRelease, max: v080, includeMin: true),
+ VersionRange(min: v123, max: v124, includeMax: true)
+ ])));
+ });
+
+ test('removes multiple portions from the same range', () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v010, max: v114),
+ VersionRange(min: v130, max: v200)
+ ]).difference(VersionConstraint.unionOf([v072, v080])),
+ equals(VersionConstraint.unionOf([
+ VersionRange(
+ min: v010, max: v072, alwaysIncludeMaxPreRelease: true),
+ VersionRange(
+ min: v072, max: v080, alwaysIncludeMaxPreRelease: true),
+ VersionRange(min: v080, max: v114),
+ VersionRange(min: v130, max: v200)
+ ])));
+ });
+
+ test('removes the same range from multiple ranges', () {
+ expect(
+ VersionConstraint.unionOf([
+ VersionRange(min: v010, max: v072),
+ VersionRange(min: v080, max: v123),
+ VersionRange(min: v124, max: v130),
+ VersionRange(min: v200, max: v234),
+ VersionRange(min: v250, max: v300)
+ ]).difference(VersionRange(min: v114, max: v201)),
+ equals(VersionConstraint.unionOf([
+ VersionRange(min: v010, max: v072),
+ VersionRange(min: v080, max: v114, includeMax: true),
+ VersionRange(
+ min: v201.firstPreRelease, max: v234, includeMin: true),
+ VersionRange(min: v250, max: v300)
+ ])));
+ });
+ });
+}
diff --git a/pkgs/source_maps/.gitignore b/pkgs/source_maps/.gitignore
new file mode 100644
index 0000000..f73b2f9
--- /dev/null
+++ b/pkgs/source_maps/.gitignore
@@ -0,0 +1,4 @@
+.dart_tool/
+.packages
+.pub/
+pubspec.lock
diff --git a/pkgs/source_maps/CHANGELOG.md b/pkgs/source_maps/CHANGELOG.md
new file mode 100644
index 0000000..ae7711e
--- /dev/null
+++ b/pkgs/source_maps/CHANGELOG.md
@@ -0,0 +1,131 @@
+## 0.10.13
+
+* Require Dart 3.3
+* Move to `dart-lang/tools` monorepo.
+
+## 0.10.12
+
+* Add additional types at API boundaries.
+
+## 0.10.11
+
+* Populate the pubspec `repository` field.
+* Update the source map documentation link in the readme.
+
+## 0.10.10
+
+* Stable release for null safety.
+
+## 0.10.9
+
+* Fix a number of document comment issues.
+* Allow parsing source map files with a missing `names` field.
+
+## 0.10.8
+
+* Preserve source-map extensions in `SingleMapping`. Extensions are keys in the
+ json map that start with `"x_"`.
+
+## 0.10.7
+
+* Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 0.10.6
+
+* Require version 2.0.0 of the Dart SDK.
+
+## 0.10.5
+
+* Add a `SingleMapping.files` field which provides access to `SourceFile`s
+ representing the `"sourcesContent"` fields in the source map.
+
+* Add an `includeSourceContents` flag to `SingleMapping.toJson()` which
+ indicates whether to include source file contents in the source map.
+
+## 0.10.4
+* Implement `highlight` in `SourceMapFileSpan`.
+* Require version `^1.3.0` of `source_span`.
+
+## 0.10.3
+ * Add `addMapping` and `containsMapping` members to `MappingBundle`.
+
+## 0.10.2
+ * Support for extended source map format.
+ * Polish `MappingBundle.spanFor` handling of URIs that have a suffix that
+ exactly match a source map in the MappingBundle.
+
+## 0.10.1+5
+ * Fix strong mode warning in test.
+
+## 0.10.1+4
+
+* Extend `MappingBundle.spanFor` to accept requests for output files that
+ don't have source maps.
+
+## 0.10.1+3
+
+* Add `MappingBundle` class that handles extended source map format that
+ supports source maps for multiple output files in a single mapper.
+ Extend `Mapping.spanFor` API to accept a uri parameter that is optional
+ for normal source maps but required for MappingBundle source maps.
+
+## 0.10.1+2
+
+* Fix more strong mode warnings.
+
+## 0.10.1+1
+
+* Fix all strong mode warnings.
+
+## 0.10.1
+
+* Add a `mapUrl` named argument to `parse` and `parseJson`. This argument is
+ used to resolve source URLs for source spans.
+
+## 0.10.0+2
+
+* Fix analyzer error (FileSpan has a new field since `source_span` 1.1.1)
+
+## 0.10.0+1
+
+* Remove an unnecessary warning printed when the "file" field is missing from a
+ Json formatted source map. This field is optional and its absence is not
+ unusual.
+
+## 0.10.0
+
+* Remove the `Span`, `Location` and `SourceFile` classes. Use the
+ corresponding `source_span` classes instead.
+
+## 0.9.4
+
+* Update `SpanFormatException` with `source` and `offset`.
+
+* All methods that take `Span`s, `Location`s, and `SourceFile`s as inputs now
+ also accept the corresponding `source_span` classes as well. Using the old
+ classes is now deprecated and will be unsupported in version 0.10.0.
+
+## 0.9.3
+
+* Support writing SingleMapping objects to source map version 3 format.
+* Support the `sourceRoot` field in the SingleMapping class.
+* Support updating the `targetUrl` field in the SingleMapping class.
+
+## 0.9.2+2
+
+* Fix a bug in `FixedSpan.getLocationMessage`.
+
+## 0.9.2+1
+
+* Minor readability improvements to `FixedSpan.getLocationMessage` and
+ `SpanException.toString`.
+
+## 0.9.2
+
+* Add `SpanException` and `SpanFormatException` classes.
+
+## 0.9.1
+
+* Support unmapped areas in source maps.
+
+* Increase the readability of location messages.
diff --git a/pkgs/source_maps/LICENSE b/pkgs/source_maps/LICENSE
new file mode 100644
index 0000000..162572a
--- /dev/null
+++ b/pkgs/source_maps/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/source_maps/README.md b/pkgs/source_maps/README.md
new file mode 100644
index 0000000..cf80291
--- /dev/null
+++ b/pkgs/source_maps/README.md
@@ -0,0 +1,25 @@
+[](https://github.com/dart-lang/tools/actions/workflows/source_maps.yaml)
+[](https://pub.dev/packages/source_maps)
+[](https://pub.dev/packages/source_maps/publisher)
+
+This project implements a Dart pub package to work with source maps.
+
+## Docs and usage
+
+The implementation is based on the [source map version 3 spec][spec] which was
+originated from the [Closure Compiler][closure] and has been implemented in
+Chrome and Firefox.
+
+In this package we provide:
+
+ * Data types defining file locations and spans: these are not part of the
+ original source map specification. These data types are great for tracking
+ source locations on source maps, but they can also be used by tools to
+ reporting useful error messages that include on source locations.
+ * A builder that creates a source map programmatically and produces the encoded
+ source map format.
+ * A parser that reads the source map format and provides APIs to read the
+ mapping information.
+
+[closure]: https://github.com/google/closure-compiler/wiki/Source-Maps
+[spec]: https://docs.google.com/a/google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
diff --git a/pkgs/source_maps/analysis_options.yaml b/pkgs/source_maps/analysis_options.yaml
new file mode 100644
index 0000000..d978f81
--- /dev/null
+++ b/pkgs/source_maps/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
diff --git a/pkgs/source_maps/lib/builder.dart b/pkgs/source_maps/lib/builder.dart
new file mode 100644
index 0000000..54ba743
--- /dev/null
+++ b/pkgs/source_maps/lib/builder.dart
@@ -0,0 +1,84 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Contains a builder object useful for creating source maps programatically.
+library source_maps.builder;
+
+// TODO(sigmund): add a builder for multi-section mappings.
+
+import 'dart:convert';
+
+import 'package:source_span/source_span.dart';
+
+import 'parser.dart';
+import 'src/source_map_span.dart';
+
+/// Builds a source map given a set of mappings.
+class SourceMapBuilder {
+ final List<Entry> _entries = <Entry>[];
+
+ /// Adds an entry mapping the [targetOffset] to [source].
+ void addFromOffset(SourceLocation source, SourceFile targetFile,
+ int targetOffset, String identifier) {
+ ArgumentError.checkNotNull(targetFile, 'targetFile');
+ _entries.add(Entry(source, targetFile.location(targetOffset), identifier));
+ }
+
+ /// Adds an entry mapping [target] to [source].
+ ///
+ /// If [isIdentifier] is true or if [target] is a [SourceMapSpan] with
+ /// `isIdentifier` set to true, this entry is considered to represent an
+ /// identifier whose value will be stored in the source map. [isIdentifier]
+ /// takes precedence over [target]'s `isIdentifier` value.
+ void addSpan(SourceSpan source, SourceSpan target, {bool? isIdentifier}) {
+ isIdentifier ??= source is SourceMapSpan ? source.isIdentifier : false;
+
+ var name = isIdentifier ? source.text : null;
+ _entries.add(Entry(source.start, target.start, name));
+ }
+
+ /// Adds an entry mapping [target] to [source].
+ void addLocation(
+ SourceLocation source, SourceLocation target, String? identifier) {
+ _entries.add(Entry(source, target, identifier));
+ }
+
+ /// Encodes all mappings added to this builder as a json map.
+ Map<String, dynamic> build(String fileUrl) {
+ return SingleMapping.fromEntries(_entries, fileUrl).toJson();
+ }
+
+ /// Encodes all mappings added to this builder as a json string.
+ String toJson(String fileUrl) => jsonEncode(build(fileUrl));
+}
+
+/// An entry in the source map builder.
+class Entry implements Comparable<Entry> {
+ /// Span denoting the original location in the input source file
+ final SourceLocation source;
+
+ /// Span indicating the corresponding location in the target file.
+ final SourceLocation target;
+
+ /// An identifier name, when this location is the start of an identifier.
+ final String? identifierName;
+
+ /// Creates a new [Entry] mapping [target] to [source].
+ Entry(this.source, this.target, this.identifierName);
+
+ /// Implements [Comparable] to ensure that entries are ordered by their
+ /// location in the target file. We sort primarily by the target offset
+ /// because source map files are encoded by printing each mapping in order as
+ /// they appear in the target file.
+ @override
+ int compareTo(Entry other) {
+ var res = target.compareTo(other.target);
+ if (res != 0) return res;
+ res = source.sourceUrl
+ .toString()
+ .compareTo(other.source.sourceUrl.toString());
+ if (res != 0) return res;
+ return source.compareTo(other.source);
+ }
+}
diff --git a/pkgs/source_maps/lib/parser.dart b/pkgs/source_maps/lib/parser.dart
new file mode 100644
index 0000000..b699ac7
--- /dev/null
+++ b/pkgs/source_maps/lib/parser.dart
@@ -0,0 +1,718 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Contains the top-level function to parse source maps version 3.
+library source_maps.parser;
+
+import 'dart:convert';
+
+import 'package:source_span/source_span.dart';
+
+import 'builder.dart' as builder;
+import 'src/source_map_span.dart';
+import 'src/utils.dart';
+import 'src/vlq.dart';
+
+/// Parses a source map directly from a json string.
+///
+/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
+/// the source map file itself. If it's passed, any URLs in the source
+/// map will be interpreted as relative to this URL when generating spans.
+// TODO(sigmund): evaluate whether other maps should have the json parsed, or
+// the string represenation.
+// TODO(tjblasi): Ignore the first line of [jsonMap] if the JSON safety string
+// `)]}'` begins the string representation of the map.
+Mapping parse(String jsonMap,
+ {Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) =>
+ parseJson(jsonDecode(jsonMap) as Map, otherMaps: otherMaps, mapUrl: mapUrl);
+
+/// Parses a source map or source map bundle directly from a json string.
+///
+/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
+/// the source map file itself. If it's passed, any URLs in the source
+/// map will be interpreted as relative to this URL when generating spans.
+Mapping parseExtended(String jsonMap,
+ {Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) =>
+ parseJsonExtended(jsonDecode(jsonMap),
+ otherMaps: otherMaps, mapUrl: mapUrl);
+
+/// Parses a source map or source map bundle.
+///
+/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
+/// the source map file itself. If it's passed, any URLs in the source
+/// map will be interpreted as relative to this URL when generating spans.
+Mapping parseJsonExtended(/*List|Map*/ Object? json,
+ {Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) {
+ if (json is List) {
+ return MappingBundle.fromJson(json, mapUrl: mapUrl);
+ }
+ return parseJson(json as Map);
+}
+
+/// Parses a source map.
+///
+/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
+/// the source map file itself. If it's passed, any URLs in the source
+/// map will be interpreted as relative to this URL when generating spans.
+Mapping parseJson(Map map,
+ {Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) {
+ if (map['version'] != 3) {
+ throw ArgumentError('unexpected source map version: ${map["version"]}. '
+ 'Only version 3 is supported.');
+ }
+
+ if (map.containsKey('sections')) {
+ if (map.containsKey('mappings') ||
+ map.containsKey('sources') ||
+ map.containsKey('names')) {
+ throw const FormatException('map containing "sections" '
+ 'cannot contain "mappings", "sources", or "names".');
+ }
+ return MultiSectionMapping.fromJson(map['sections'] as List, otherMaps,
+ mapUrl: mapUrl);
+ }
+ return SingleMapping.fromJson(map.cast<String, dynamic>(), mapUrl: mapUrl);
+}
+
+/// A mapping parsed out of a source map.
+abstract class Mapping {
+ /// Returns the span associated with [line] and [column].
+ ///
+ /// [uri] is the optional location of the output file to find the span for
+ /// to disambiguate cases where a mapping may have different mappings for
+ /// different output files.
+ SourceMapSpan? spanFor(int line, int column,
+ {Map<String, SourceFile>? files, String? uri});
+
+ /// Returns the span associated with [location].
+ SourceMapSpan? spanForLocation(SourceLocation location,
+ {Map<String, SourceFile>? files}) {
+ return spanFor(location.line, location.column,
+ uri: location.sourceUrl?.toString(), files: files);
+ }
+}
+
+/// A meta-level map containing sections.
+class MultiSectionMapping extends Mapping {
+ /// For each section, the start line offset.
+ final List<int> _lineStart = <int>[];
+
+ /// For each section, the start column offset.
+ final List<int> _columnStart = <int>[];
+
+ /// For each section, the actual source map information, which is not adjusted
+ /// for offsets.
+ final List<Mapping> _maps = <Mapping>[];
+
+ /// Creates a section mapping from json.
+ MultiSectionMapping.fromJson(List sections, Map<String, Map>? otherMaps,
+ {/*String|Uri*/ Object? mapUrl}) {
+ for (var section in sections.cast<Map>()) {
+ var offset = section['offset'] as Map?;
+ if (offset == null) throw const FormatException('section missing offset');
+
+ var line = offset['line'] as int?;
+ if (line == null) throw const FormatException('offset missing line');
+
+ var column = offset['column'] as int?;
+ if (column == null) throw const FormatException('offset missing column');
+
+ _lineStart.add(line);
+ _columnStart.add(column);
+
+ var url = section['url'] as String?;
+ var map = section['map'] as Map?;
+
+ if (url != null && map != null) {
+ throw const FormatException(
+ "section can't use both url and map entries");
+ } else if (url != null) {
+ var other = otherMaps?[url];
+ if (otherMaps == null || other == null) {
+ throw FormatException(
+ 'section contains refers to $url, but no map was '
+ 'given for it. Make sure a map is passed in "otherMaps"');
+ }
+ _maps.add(parseJson(other, otherMaps: otherMaps, mapUrl: url));
+ } else if (map != null) {
+ _maps.add(parseJson(map, otherMaps: otherMaps, mapUrl: mapUrl));
+ } else {
+ throw const FormatException('section missing url or map');
+ }
+ }
+ if (_lineStart.isEmpty) {
+ throw const FormatException('expected at least one section');
+ }
+ }
+
+ int _indexFor(int line, int column) {
+ for (var i = 0; i < _lineStart.length; i++) {
+ if (line < _lineStart[i]) return i - 1;
+ if (line == _lineStart[i] && column < _columnStart[i]) return i - 1;
+ }
+ return _lineStart.length - 1;
+ }
+
+ @override
+ SourceMapSpan? spanFor(int line, int column,
+ {Map<String, SourceFile>? files, String? uri}) {
+ // TODO(jacobr): perhaps verify that targetUrl matches the actual uri
+ // or at least ends in the same file name.
+ var index = _indexFor(line, column);
+ return _maps[index].spanFor(
+ line - _lineStart[index], column - _columnStart[index],
+ files: files);
+ }
+
+ @override
+ String toString() {
+ var buff = StringBuffer('$runtimeType : [');
+ for (var i = 0; i < _lineStart.length; i++) {
+ buff
+ ..write('(')
+ ..write(_lineStart[i])
+ ..write(',')
+ ..write(_columnStart[i])
+ ..write(':')
+ ..write(_maps[i])
+ ..write(')');
+ }
+ buff.write(']');
+ return buff.toString();
+ }
+}
+
+class MappingBundle extends Mapping {
+ final Map<String, SingleMapping> _mappings = {};
+
+ MappingBundle();
+
+ MappingBundle.fromJson(List json, {/*String|Uri*/ Object? mapUrl}) {
+ for (var map in json) {
+ addMapping(parseJson(map as Map, mapUrl: mapUrl) as SingleMapping);
+ }
+ }
+
+ void addMapping(SingleMapping mapping) {
+ // TODO(jacobr): verify that targetUrl is valid uri instead of a windows
+ // path.
+ // TODO: Remove type arg https://github.com/dart-lang/sdk/issues/42227
+ var targetUrl = ArgumentError.checkNotNull<String>(
+ mapping.targetUrl, 'mapping.targetUrl');
+ _mappings[targetUrl] = mapping;
+ }
+
+ /// Encodes the Mapping mappings as a json map.
+ List toJson() => _mappings.values.map((v) => v.toJson()).toList();
+
+ @override
+ String toString() {
+ var buff = StringBuffer();
+ for (var map in _mappings.values) {
+ buff.write(map.toString());
+ }
+ return buff.toString();
+ }
+
+ bool containsMapping(String url) => _mappings.containsKey(url);
+
+ @override
+ SourceMapSpan? spanFor(int line, int column,
+ {Map<String, SourceFile>? files, String? uri}) {
+ // TODO: Remove type arg https://github.com/dart-lang/sdk/issues/42227
+ uri = ArgumentError.checkNotNull<String>(uri, 'uri');
+
+ // Find the longest suffix of the uri that matches the sourcemap
+ // where the suffix starts after a path segment boundary.
+ // We consider ":" and "/" as path segment boundaries so that
+ // "package:" uris can be handled with minimal special casing. Having a
+ // few false positive path segment boundaries is not a significant issue
+ // as we prefer the longest matching prefix.
+ // Using package:path `path.split` to find path segment boundaries would
+ // not generate all of the path segment boundaries we want for "package:"
+ // urls as "package:package_name" would be one path segment when we want
+ // "package" and "package_name" to be sepearate path segments.
+
+ var onBoundary = true;
+ var separatorCodeUnits = ['/'.codeUnitAt(0), ':'.codeUnitAt(0)];
+ for (var i = 0; i < uri.length; ++i) {
+ if (onBoundary) {
+ var candidate = uri.substring(i);
+ var candidateMapping = _mappings[candidate];
+ if (candidateMapping != null) {
+ return candidateMapping.spanFor(line, column,
+ files: files, uri: candidate);
+ }
+ }
+ onBoundary = separatorCodeUnits.contains(uri.codeUnitAt(i));
+ }
+
+ // Note: when there is no source map for an uri, this behaves like an
+ // identity function, returning the requested location as the result.
+
+ // Create a mock offset for the output location. We compute it in terms
+ // of the input line and column to minimize the chances that two different
+ // line and column locations are mapped to the same offset.
+ var offset = line * 1000000 + column;
+ var location = SourceLocation(offset,
+ line: line, column: column, sourceUrl: Uri.parse(uri));
+ return SourceMapSpan(location, location, '');
+ }
+}
+
+/// A map containing direct source mappings.
+class SingleMapping extends Mapping {
+ /// Source urls used in the mapping, indexed by id.
+ final List<String> urls;
+
+ /// Source names used in the mapping, indexed by id.
+ final List<String> names;
+
+ /// The [SourceFile]s to which the entries in [lines] refer.
+ ///
+ /// This is in the same order as [urls]. If this was constructed using
+ /// [SingleMapping.fromEntries], this contains files from any [FileLocation]s
+ /// used to build the mapping. If it was parsed from JSON, it contains files
+ /// for any sources whose contents were provided via the `"sourcesContent"`
+ /// field.
+ ///
+ /// Files whose contents aren't available are `null`.
+ final List<SourceFile?> files;
+
+ /// Entries indicating the beginning of each span.
+ final List<TargetLineEntry> lines;
+
+ /// Url of the target file.
+ String? targetUrl;
+
+ /// Source root prepended to all entries in [urls].
+ String? sourceRoot;
+
+ final Uri? _mapUrl;
+
+ final Map<String, dynamic> extensions;
+
+ SingleMapping._(this.targetUrl, this.files, this.urls, this.names, this.lines)
+ : _mapUrl = null,
+ extensions = {};
+
+ factory SingleMapping.fromEntries(Iterable<builder.Entry> entries,
+ [String? fileUrl]) {
+ // The entries needs to be sorted by the target offsets.
+ var sourceEntries = entries.toList()..sort();
+ var lines = <TargetLineEntry>[];
+
+ // Indices associated with file urls that will be part of the source map. We
+ // rely on map order so that `urls.keys[urls[u]] == u`
+ var urls = <String, int>{};
+
+ // Indices associated with identifiers that will be part of the source map.
+ // We rely on map order so that `names.keys[names[n]] == n`
+ var names = <String, int>{};
+
+ /// The file for each URL, indexed by [urls]' values.
+ var files = <int, SourceFile>{};
+
+ int? lineNum;
+ late List<TargetEntry> targetEntries;
+ for (var sourceEntry in sourceEntries) {
+ if (lineNum == null || sourceEntry.target.line > lineNum) {
+ lineNum = sourceEntry.target.line;
+ targetEntries = <TargetEntry>[];
+ lines.add(TargetLineEntry(lineNum, targetEntries));
+ }
+
+ var sourceUrl = sourceEntry.source.sourceUrl;
+ var urlId = urls.putIfAbsent(
+ sourceUrl == null ? '' : sourceUrl.toString(), () => urls.length);
+
+ if (sourceEntry.source is FileLocation) {
+ files.putIfAbsent(
+ urlId, () => (sourceEntry.source as FileLocation).file);
+ }
+
+ var sourceEntryIdentifierName = sourceEntry.identifierName;
+ var srcNameId = sourceEntryIdentifierName == null
+ ? null
+ : names.putIfAbsent(sourceEntryIdentifierName, () => names.length);
+ targetEntries.add(TargetEntry(sourceEntry.target.column, urlId,
+ sourceEntry.source.line, sourceEntry.source.column, srcNameId));
+ }
+ return SingleMapping._(fileUrl, urls.values.map((i) => files[i]).toList(),
+ urls.keys.toList(), names.keys.toList(), lines);
+ }
+
+ SingleMapping.fromJson(Map<String, dynamic> map, {Object? mapUrl})
+ : targetUrl = map['file'] as String?,
+ urls = List<String>.from(map['sources'] as List),
+ names = List<String>.from((map['names'] as List?) ?? []),
+ files = List.filled((map['sources'] as List).length, null),
+ sourceRoot = map['sourceRoot'] as String?,
+ lines = <TargetLineEntry>[],
+ _mapUrl = mapUrl is String ? Uri.parse(mapUrl) : (mapUrl as Uri?),
+ extensions = {} {
+ var sourcesContent = map['sourcesContent'] == null
+ ? const <String?>[]
+ : List<String?>.from(map['sourcesContent'] as List);
+ for (var i = 0; i < urls.length && i < sourcesContent.length; i++) {
+ var source = sourcesContent[i];
+ if (source == null) continue;
+ files[i] = SourceFile.fromString(source, url: urls[i]);
+ }
+
+ var line = 0;
+ var column = 0;
+ var srcUrlId = 0;
+ var srcLine = 0;
+ var srcColumn = 0;
+ var srcNameId = 0;
+ var tokenizer = _MappingTokenizer(map['mappings'] as String);
+ var entries = <TargetEntry>[];
+
+ while (tokenizer.hasTokens) {
+ if (tokenizer.nextKind.isNewLine) {
+ if (entries.isNotEmpty) {
+ lines.add(TargetLineEntry(line, entries));
+ entries = <TargetEntry>[];
+ }
+ line++;
+ column = 0;
+ tokenizer._consumeNewLine();
+ continue;
+ }
+
+ // Decode the next entry, using the previous encountered values to
+ // decode the relative values.
+ //
+ // We expect 1, 4, or 5 values. If present, values are expected in the
+ // following order:
+ // 0: the starting column in the current line of the generated file
+ // 1: the id of the original source file
+ // 2: the starting line in the original source
+ // 3: the starting column in the original source
+ // 4: the id of the original symbol name
+ // The values are relative to the previous encountered values.
+ if (tokenizer.nextKind.isNewSegment) throw _segmentError(0, line);
+ column += tokenizer._consumeValue();
+ if (!tokenizer.nextKind.isValue) {
+ entries.add(TargetEntry(column));
+ } else {
+ srcUrlId += tokenizer._consumeValue();
+ if (srcUrlId >= urls.length) {
+ throw StateError(
+ 'Invalid source url id. $targetUrl, $line, $srcUrlId');
+ }
+ if (!tokenizer.nextKind.isValue) throw _segmentError(2, line);
+ srcLine += tokenizer._consumeValue();
+ if (!tokenizer.nextKind.isValue) throw _segmentError(3, line);
+ srcColumn += tokenizer._consumeValue();
+ if (!tokenizer.nextKind.isValue) {
+ entries.add(TargetEntry(column, srcUrlId, srcLine, srcColumn));
+ } else {
+ srcNameId += tokenizer._consumeValue();
+ if (srcNameId >= names.length) {
+ throw StateError('Invalid name id: $targetUrl, $line, $srcNameId');
+ }
+ entries.add(
+ TargetEntry(column, srcUrlId, srcLine, srcColumn, srcNameId));
+ }
+ }
+ if (tokenizer.nextKind.isNewSegment) tokenizer._consumeNewSegment();
+ }
+ if (entries.isNotEmpty) {
+ lines.add(TargetLineEntry(line, entries));
+ }
+
+ map.forEach((name, value) {
+ if (name.startsWith('x_')) extensions[name] = value;
+ });
+ }
+
+ /// Encodes the Mapping mappings as a json map.
+ ///
+ /// If [includeSourceContents] is `true`, this includes the source file
+ /// contents from [files] in the map if possible.
+ Map<String, dynamic> toJson({bool includeSourceContents = false}) {
+ var buff = StringBuffer();
+ var line = 0;
+ var column = 0;
+ var srcLine = 0;
+ var srcColumn = 0;
+ var srcUrlId = 0;
+ var srcNameId = 0;
+ var first = true;
+
+ for (var entry in lines) {
+ var nextLine = entry.line;
+ if (nextLine > line) {
+ for (var i = line; i < nextLine; ++i) {
+ buff.write(';');
+ }
+ line = nextLine;
+ column = 0;
+ first = true;
+ }
+
+ for (var segment in entry.entries) {
+ if (!first) buff.write(',');
+ first = false;
+ column = _append(buff, column, segment.column);
+
+ // Encoding can be just the column offset if there is no source
+ // information.
+ var newUrlId = segment.sourceUrlId;
+ if (newUrlId == null) continue;
+ srcUrlId = _append(buff, srcUrlId, newUrlId);
+ srcLine = _append(buff, srcLine, segment.sourceLine!);
+ srcColumn = _append(buff, srcColumn, segment.sourceColumn!);
+
+ if (segment.sourceNameId == null) continue;
+ srcNameId = _append(buff, srcNameId, segment.sourceNameId!);
+ }
+ }
+
+ var result = <String, dynamic>{
+ 'version': 3,
+ 'sourceRoot': sourceRoot ?? '',
+ 'sources': urls,
+ 'names': names,
+ 'mappings': buff.toString(),
+ };
+ if (targetUrl != null) result['file'] = targetUrl!;
+
+ if (includeSourceContents) {
+ result['sourcesContent'] = files.map((file) => file?.getText(0)).toList();
+ }
+ extensions.forEach((name, value) => result[name] = value);
+
+ return result;
+ }
+
+ /// Appends to [buff] a VLQ encoding of [newValue] using the difference
+ /// between [oldValue] and [newValue]
+ static int _append(StringBuffer buff, int oldValue, int newValue) {
+ buff.writeAll(encodeVlq(newValue - oldValue));
+ return newValue;
+ }
+
+ StateError _segmentError(int seen, int line) =>
+ StateError('Invalid entry in sourcemap, expected 1, 4, or 5'
+ ' values, but got $seen.\ntargeturl: $targetUrl, line: $line');
+
+ /// Returns [TargetLineEntry] which includes the location in the target [line]
+ /// number. In particular, the resulting entry is the last entry whose line
+ /// number is lower or equal to [line].
+ TargetLineEntry? _findLine(int line) {
+ var index = binarySearch(lines, (e) => e.line > line);
+ return (index <= 0) ? null : lines[index - 1];
+ }
+
+ /// Returns [TargetEntry] which includes the location denoted by
+ /// [line], [column]. If [lineEntry] corresponds to [line], then this will be
+ /// the last entry whose column is lower or equal than [column]. If
+ /// [lineEntry] corresponds to a line prior to [line], then the result will be
+ /// the very last entry on that line.
+ TargetEntry? _findColumn(int line, int column, TargetLineEntry? lineEntry) {
+ if (lineEntry == null || lineEntry.entries.isEmpty) return null;
+ if (lineEntry.line != line) return lineEntry.entries.last;
+ var entries = lineEntry.entries;
+ var index = binarySearch(entries, (e) => e.column > column);
+ return (index <= 0) ? null : entries[index - 1];
+ }
+
+ @override
+ SourceMapSpan? spanFor(int line, int column,
+ {Map<String, SourceFile>? files, String? uri}) {
+ var entry = _findColumn(line, column, _findLine(line));
+ if (entry == null) return null;
+
+ var sourceUrlId = entry.sourceUrlId;
+ if (sourceUrlId == null) return null;
+
+ var url = urls[sourceUrlId];
+ if (sourceRoot != null) {
+ url = '$sourceRoot$url';
+ }
+
+ var sourceNameId = entry.sourceNameId;
+ var file = files?[url];
+ if (file != null) {
+ var start = file.getOffset(entry.sourceLine!, entry.sourceColumn);
+ if (sourceNameId != null) {
+ var text = names[sourceNameId];
+ return SourceMapFileSpan(file.span(start, start + text.length),
+ isIdentifier: true);
+ } else {
+ return SourceMapFileSpan(file.location(start).pointSpan());
+ }
+ } else {
+ var start = SourceLocation(0,
+ sourceUrl: _mapUrl?.resolve(url) ?? url,
+ line: entry.sourceLine,
+ column: entry.sourceColumn);
+
+ // Offset and other context is not available.
+ if (sourceNameId != null) {
+ return SourceMapSpan.identifier(start, names[sourceNameId]);
+ } else {
+ return SourceMapSpan(start, start, '');
+ }
+ }
+ }
+
+ @override
+ String toString() {
+ return (StringBuffer('$runtimeType : [')
+ ..write('targetUrl: ')
+ ..write(targetUrl)
+ ..write(', sourceRoot: ')
+ ..write(sourceRoot)
+ ..write(', urls: ')
+ ..write(urls)
+ ..write(', names: ')
+ ..write(names)
+ ..write(', lines: ')
+ ..write(lines)
+ ..write(']'))
+ .toString();
+ }
+
+ String get debugString {
+ var buff = StringBuffer();
+ for (var lineEntry in lines) {
+ var line = lineEntry.line;
+ for (var entry in lineEntry.entries) {
+ buff
+ ..write(targetUrl)
+ ..write(': ')
+ ..write(line)
+ ..write(':')
+ ..write(entry.column);
+ var sourceUrlId = entry.sourceUrlId;
+ if (sourceUrlId != null) {
+ buff
+ ..write(' --> ')
+ ..write(sourceRoot)
+ ..write(urls[sourceUrlId])
+ ..write(': ')
+ ..write(entry.sourceLine)
+ ..write(':')
+ ..write(entry.sourceColumn);
+ }
+ var sourceNameId = entry.sourceNameId;
+ if (sourceNameId != null) {
+ buff
+ ..write(' (')
+ ..write(names[sourceNameId])
+ ..write(')');
+ }
+ buff.write('\n');
+ }
+ }
+ return buff.toString();
+ }
+}
+
+/// A line entry read from a source map.
+class TargetLineEntry {
+ final int line;
+ List<TargetEntry> entries;
+ TargetLineEntry(this.line, this.entries);
+
+ @override
+ String toString() => '$runtimeType: $line $entries';
+}
+
+/// A target segment entry read from a source map
+class TargetEntry {
+ final int column;
+ final int? sourceUrlId;
+ final int? sourceLine;
+ final int? sourceColumn;
+ final int? sourceNameId;
+
+ TargetEntry(this.column,
+ [this.sourceUrlId,
+ this.sourceLine,
+ this.sourceColumn,
+ this.sourceNameId]);
+
+ @override
+ String toString() => '$runtimeType: '
+ '($column, $sourceUrlId, $sourceLine, $sourceColumn, $sourceNameId)';
+}
+
+/// A character iterator over a string that can peek one character ahead.
+class _MappingTokenizer implements Iterator<String> {
+ final String _internal;
+ final int _length;
+ int index = -1;
+ _MappingTokenizer(String internal)
+ : _internal = internal,
+ _length = internal.length;
+
+ // Iterator API is used by decodeVlq to consume VLQ entries.
+ @override
+ bool moveNext() => ++index < _length;
+
+ @override
+ String get current => (index >= 0 && index < _length)
+ ? _internal[index]
+ : throw RangeError.index(index, _internal);
+
+ bool get hasTokens => index < _length - 1 && _length > 0;
+
+ _TokenKind get nextKind {
+ if (!hasTokens) return _TokenKind.eof;
+ var next = _internal[index + 1];
+ if (next == ';') return _TokenKind.line;
+ if (next == ',') return _TokenKind.segment;
+ return _TokenKind.value;
+ }
+
+ int _consumeValue() => decodeVlq(this);
+ void _consumeNewLine() {
+ ++index;
+ }
+
+ void _consumeNewSegment() {
+ ++index;
+ }
+
+ // Print the state of the iterator, with colors indicating the current
+ // position.
+ @override
+ String toString() {
+ var buff = StringBuffer();
+ for (var i = 0; i < index; i++) {
+ buff.write(_internal[i]);
+ }
+ buff.write('[31m');
+ try {
+ buff.write(current);
+ // TODO: Determine whether this try / catch can be removed.
+ // ignore: avoid_catching_errors
+ } on RangeError catch (_) {}
+ buff.write('[0m');
+ for (var i = index + 1; i < _internal.length; i++) {
+ buff.write(_internal[i]);
+ }
+ buff.write(' ($index)');
+ return buff.toString();
+ }
+}
+
+class _TokenKind {
+ static const _TokenKind line = _TokenKind(isNewLine: true);
+ static const _TokenKind segment = _TokenKind(isNewSegment: true);
+ static const _TokenKind eof = _TokenKind(isEof: true);
+ static const _TokenKind value = _TokenKind();
+ final bool isNewLine;
+ final bool isNewSegment;
+ final bool isEof;
+ bool get isValue => !isNewLine && !isNewSegment && !isEof;
+
+ const _TokenKind(
+ {this.isNewLine = false, this.isNewSegment = false, this.isEof = false});
+}
diff --git a/pkgs/source_maps/lib/printer.dart b/pkgs/source_maps/lib/printer.dart
new file mode 100644
index 0000000..17733cd
--- /dev/null
+++ b/pkgs/source_maps/lib/printer.dart
@@ -0,0 +1,262 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Contains a code printer that generates code by recording the source maps.
+library source_maps.printer;
+
+import 'package:source_span/source_span.dart';
+
+import 'builder.dart';
+import 'src/source_map_span.dart';
+import 'src/utils.dart';
+
+/// A simple printer that keeps track of offset locations and records source
+/// maps locations.
+class Printer {
+ final String filename;
+ final StringBuffer _buff = StringBuffer();
+ final SourceMapBuilder _maps = SourceMapBuilder();
+ String get text => _buff.toString();
+ String get map => _maps.toJson(filename);
+
+ /// Current source location mapping.
+ SourceLocation? _loc;
+
+ /// Current line in the buffer;
+ int _line = 0;
+
+ /// Current column in the buffer.
+ int _column = 0;
+
+ Printer(this.filename);
+
+ /// Add [str] contents to the output, tracking new lines to track correct
+ /// positions for span locations. When [projectMarks] is true, this method
+ /// adds a source map location on each new line, projecting that every new
+ /// line in the target file (printed here) corresponds to a new line in the
+ /// source file.
+ void add(String str, {bool projectMarks = false}) {
+ var chars = str.runes.toList();
+ var length = chars.length;
+ for (var i = 0; i < length; i++) {
+ var c = chars[i];
+ if (c == lineFeed ||
+ (c == carriageReturn &&
+ (i + 1 == length || chars[i + 1] != lineFeed))) {
+ // Return not followed by line-feed is treated as a new line.
+ _line++;
+ _column = 0;
+ {
+ // **Warning**: Any calls to `mark` will change the value of `_loc`,
+ // so this local variable is no longer up to date after that point.
+ //
+ // This is why it has been put inside its own block to limit the
+ // scope in which it is available.
+ var loc = _loc;
+ if (projectMarks && loc != null) {
+ if (loc is FileLocation) {
+ var file = loc.file;
+ mark(file.location(file.getOffset(loc.line + 1)));
+ } else {
+ mark(SourceLocation(0,
+ sourceUrl: loc.sourceUrl, line: loc.line + 1, column: 0));
+ }
+ }
+ }
+ } else {
+ _column++;
+ }
+ }
+ _buff.write(str);
+ }
+
+ /// Append a [total] number of spaces in the target file. Typically used for
+ /// formatting indentation.
+ void addSpaces(int total) {
+ for (var i = 0; i < total; i++) {
+ _buff.write(' ');
+ }
+ _column += total;
+ }
+
+ /// Marks that the current point in the target file corresponds to the [mark]
+ /// in the source file, which can be either a [SourceLocation] or a
+ /// [SourceSpan]. When the mark is a [SourceMapSpan] with `isIdentifier` set,
+ /// this also records the name of the identifier in the source map
+ /// information.
+ void mark(Object mark) {
+ late final SourceLocation loc;
+ String? identifier;
+ if (mark is SourceLocation) {
+ loc = mark;
+ } else if (mark is SourceSpan) {
+ loc = mark.start;
+ if (mark is SourceMapSpan && mark.isIdentifier) identifier = mark.text;
+ }
+ _maps.addLocation(loc,
+ SourceLocation(_buff.length, line: _line, column: _column), identifier);
+ _loc = loc;
+ }
+}
+
+/// A more advanced printer that keeps track of offset locations to record
+/// source maps, but additionally allows nesting of different kind of items,
+/// including [NestedPrinter]s, and it let's you automatically indent text.
+///
+/// This class is especially useful when doing code generation, where different
+/// pieces of the code are generated independently on separate printers, and are
+/// finally put together in the end.
+class NestedPrinter implements NestedItem {
+ /// Items recoded by this printer, which can be [String] literals,
+ /// [NestedItem]s, and source map information like [SourceLocation] and
+ /// [SourceSpan].
+ final List<Object> _items = [];
+
+ /// Internal buffer to merge consecutive strings added to this printer.
+ StringBuffer? _buff;
+
+ /// Current indentation, which can be updated from outside this class.
+ int indent;
+
+ /// [Printer] used during the last call to [build], if any.
+ Printer? printer;
+
+ /// Returns the text produced after calling [build].
+ String? get text => printer?.text;
+
+ /// Returns the source-map information produced after calling [build].
+ String? get map => printer?.map;
+
+ /// Item used to indicate that the following item is copied from the original
+ /// source code, and hence we should preserve source-maps on every new line.
+ static final _original = Object();
+
+ NestedPrinter([this.indent = 0]);
+
+ /// Adds [object] to this printer. [object] can be a [String],
+ /// [NestedPrinter], or anything implementing [NestedItem]. If [object] is a
+ /// [String], the value is appended directly, without doing any formatting
+ /// changes. If you wish to add a line of code with automatic indentation, use
+ /// [addLine] instead. [NestedPrinter]s and [NestedItem]s are not processed
+ /// until [build] gets called later on. We ensure that [build] emits every
+ /// object in the order that they were added to this printer.
+ ///
+ /// The [location] and [span] parameters indicate the corresponding source map
+ /// location of [object] in the original input. Only one, [location] or
+ /// [span], should be provided at a time.
+ ///
+ /// Indicate [isOriginal] when [object] is copied directly from the user code.
+ /// Setting [isOriginal] will make this printer propagate source map locations
+ /// on every line-break.
+ void add(Object object,
+ {SourceLocation? location, SourceSpan? span, bool isOriginal = false}) {
+ if (object is! String || location != null || span != null || isOriginal) {
+ _flush();
+ assert(location == null || span == null);
+ if (location != null) _items.add(location);
+ if (span != null) _items.add(span);
+ if (isOriginal) _items.add(_original);
+ }
+
+ if (object is String) {
+ _appendString(object);
+ } else {
+ _items.add(object);
+ }
+ }
+
+ /// Append `2 * indent` spaces to this printer.
+ void insertIndent() => _indent(indent);
+
+ /// Add a [line], autoindenting to the current value of [indent]. Note,
+ /// indentation is not inferred from the contents added to this printer. If a
+ /// line starts or ends an indentation block, you need to also update [indent]
+ /// accordingly. Also, indentation is not adapted for nested printers. If
+ /// you add a [NestedPrinter] to this printer, its indentation is set
+ /// separately and will not include any the indentation set here.
+ ///
+ /// The [location] and [span] parameters indicate the corresponding source map
+ /// location of [line] in the original input. Only one, [location] or
+ /// [span], should be provided at a time.
+ void addLine(String? line, {SourceLocation? location, SourceSpan? span}) {
+ if (location != null || span != null) {
+ _flush();
+ assert(location == null || span == null);
+ if (location != null) _items.add(location);
+ if (span != null) _items.add(span);
+ }
+ if (line == null) return;
+ if (line != '') {
+ // We don't indent empty lines.
+ _indent(indent);
+ _appendString(line);
+ }
+ _appendString('\n');
+ }
+
+ /// Appends a string merging it with any previous strings, if possible.
+ void _appendString(String s) {
+ var buf = _buff ??= StringBuffer();
+ buf.write(s);
+ }
+
+ /// Adds all of the current [_buff] contents as a string item.
+ void _flush() {
+ if (_buff != null) {
+ _items.add(_buff.toString());
+ _buff = null;
+ }
+ }
+
+ void _indent(int indent) {
+ for (var i = 0; i < indent; i++) {
+ _appendString(' ');
+ }
+ }
+
+ /// Returns a string representation of all the contents appended to this
+ /// printer, including source map location tokens.
+ @override
+ String toString() {
+ _flush();
+ return (StringBuffer()..writeAll(_items)).toString();
+ }
+
+ /// Builds the output of this printer and source map information. After
+ /// calling this function, you can use [text] and [map] to retrieve the
+ /// geenrated code and source map information, respectively.
+ void build(String filename) {
+ writeTo(printer = Printer(filename));
+ }
+
+ /// Implements the [NestedItem] interface.
+ @override
+ void writeTo(Printer printer) {
+ _flush();
+ var propagate = false;
+ for (var item in _items) {
+ if (item is NestedItem) {
+ item.writeTo(printer);
+ } else if (item is String) {
+ printer.add(item, projectMarks: propagate);
+ propagate = false;
+ } else if (item is SourceLocation || item is SourceSpan) {
+ printer.mark(item);
+ } else if (item == _original) {
+ // we insert booleans when we are about to quote text that was copied
+ // from the original source. In such case, we will propagate marks on
+ // every new-line.
+ propagate = true;
+ } else {
+ throw UnsupportedError('Unknown item type: $item');
+ }
+ }
+ }
+}
+
+/// An item added to a [NestedPrinter].
+abstract class NestedItem {
+ /// Write the contents of this item into [printer].
+ void writeTo(Printer printer);
+}
diff --git a/pkgs/source_maps/lib/refactor.dart b/pkgs/source_maps/lib/refactor.dart
new file mode 100644
index 0000000..98e0c93
--- /dev/null
+++ b/pkgs/source_maps/lib/refactor.dart
@@ -0,0 +1,140 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Tools to help implement refactoring like transformations to Dart code.
+///
+/// [TextEditTransaction] supports making a series of changes to a text buffer.
+/// [guessIndent] helps to guess the appropriate indentiation for the new code.
+library source_maps.refactor;
+
+import 'package:source_span/source_span.dart';
+
+import 'printer.dart';
+import 'src/utils.dart';
+
+/// Editable text transaction.
+///
+/// Applies a series of edits using original location
+/// information, and composes them into the edited string.
+class TextEditTransaction {
+ final SourceFile? file;
+ final String original;
+ final _edits = <_TextEdit>[];
+
+ /// Creates a new transaction.
+ TextEditTransaction(this.original, this.file);
+
+ bool get hasEdits => _edits.isNotEmpty;
+
+ /// Edit the original text, replacing text on the range [begin] and [end]
+ /// with the [replacement]. [replacement] can be either a string or a
+ /// [NestedPrinter].
+ void edit(int begin, int end, Object replacement) {
+ _edits.add(_TextEdit(begin, end, replacement));
+ }
+
+ /// Create a source map [SourceLocation] for [offset], if [file] is not
+ /// `null`.
+ SourceLocation? _loc(int offset) => file?.location(offset);
+
+ /// Applies all pending [edit]s and returns a [NestedPrinter] containing the
+ /// rewritten string and source map information. [file]`.location` is given to
+ /// the underlying printer to indicate the name of the generated file that
+ /// will contains the source map information.
+ ///
+ /// Throws [UnsupportedError] if the edits were overlapping. If no edits were
+ /// made, the printer simply contains the original string.
+ NestedPrinter commit() {
+ var printer = NestedPrinter();
+ if (_edits.isEmpty) {
+ return printer..add(original, location: _loc(0), isOriginal: true);
+ }
+
+ // Sort edits by start location.
+ _edits.sort();
+
+ var consumed = 0;
+ for (var edit in _edits) {
+ if (consumed > edit.begin) {
+ var sb = StringBuffer();
+ sb
+ ..write(file?.location(edit.begin).toolString)
+ ..write(': overlapping edits. Insert at offset ')
+ ..write(edit.begin)
+ ..write(' but have consumed ')
+ ..write(consumed)
+ ..write(' input characters. List of edits:');
+ for (var e in _edits) {
+ sb
+ ..write('\n ')
+ ..write(e);
+ }
+ throw UnsupportedError(sb.toString());
+ }
+
+ // Add characters from the original string between this edit and the last
+ // one, if any.
+ var betweenEdits = original.substring(consumed, edit.begin);
+ printer
+ ..add(betweenEdits, location: _loc(consumed), isOriginal: true)
+ ..add(edit.replace, location: _loc(edit.begin));
+ consumed = edit.end;
+ }
+
+ // Add any text from the end of the original string that was not replaced.
+ printer.add(original.substring(consumed),
+ location: _loc(consumed), isOriginal: true);
+ return printer;
+ }
+}
+
+class _TextEdit implements Comparable<_TextEdit> {
+ final int begin;
+ final int end;
+
+ /// The replacement used by the edit, can be a string or a [NestedPrinter].
+ final Object replace;
+
+ _TextEdit(this.begin, this.end, this.replace);
+
+ int get length => end - begin;
+
+ @override
+ String toString() => '(Edit @ $begin,$end: "$replace")';
+
+ @override
+ int compareTo(_TextEdit other) {
+ var diff = begin - other.begin;
+ if (diff != 0) return diff;
+ return end - other.end;
+ }
+}
+
+/// Returns all whitespace characters at the start of [charOffset]'s line.
+String guessIndent(String code, int charOffset) {
+ // Find the beginning of the line
+ var lineStart = 0;
+ for (var i = charOffset - 1; i >= 0; i--) {
+ var c = code.codeUnitAt(i);
+ if (c == lineFeed || c == carriageReturn) {
+ lineStart = i + 1;
+ break;
+ }
+ }
+
+ // Grab all the whitespace
+ var whitespaceEnd = code.length;
+ for (var i = lineStart; i < code.length; i++) {
+ var c = code.codeUnitAt(i);
+ if (c != _space && c != _tab) {
+ whitespaceEnd = i;
+ break;
+ }
+ }
+
+ return code.substring(lineStart, whitespaceEnd);
+}
+
+const int _tab = 9;
+const int _space = 32;
diff --git a/pkgs/source_maps/lib/source_maps.dart b/pkgs/source_maps/lib/source_maps.dart
new file mode 100644
index 0000000..58f805a
--- /dev/null
+++ b/pkgs/source_maps/lib/source_maps.dart
@@ -0,0 +1,38 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Library to create and parse source maps.
+///
+/// Create a source map using [SourceMapBuilder]. For example:
+///
+/// ```dart
+/// var json = (new SourceMapBuilder()
+/// ..add(inputSpan1, outputSpan1)
+/// ..add(inputSpan2, outputSpan2)
+/// ..add(inputSpan3, outputSpan3)
+/// .toJson(outputFile);
+/// ```
+///
+/// Use the source_span package's [SourceSpan] and [SourceFile] classes to
+/// specify span locations.
+///
+/// Parse a source map using [parse], and call `spanFor` on the returned mapping
+/// object. For example:
+///
+/// ```dart
+/// var mapping = parse(json);
+/// mapping.spanFor(outputSpan1.line, outputSpan1.column)
+/// ```
+library source_maps;
+
+import 'package:source_span/source_span.dart';
+
+import 'builder.dart';
+import 'parser.dart';
+
+export 'builder.dart';
+export 'parser.dart';
+export 'printer.dart';
+export 'refactor.dart';
+export 'src/source_map_span.dart';
diff --git a/pkgs/source_maps/lib/src/source_map_span.dart b/pkgs/source_maps/lib/src/source_map_span.dart
new file mode 100644
index 0000000..aad8a32
--- /dev/null
+++ b/pkgs/source_maps/lib/src/source_map_span.dart
@@ -0,0 +1,72 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_span/source_span.dart';
+
+/// A [SourceSpan] for spans coming from or being written to source maps.
+///
+/// These spans have an extra piece of metadata: whether or not they represent
+/// an identifier (see [isIdentifier]).
+class SourceMapSpan extends SourceSpanBase {
+ /// Whether this span represents an identifier.
+ ///
+ /// If this is `true`, [text] is the value of the identifier.
+ final bool isIdentifier;
+
+ SourceMapSpan(super.start, super.end, super.text,
+ {this.isIdentifier = false});
+
+ /// Creates a [SourceMapSpan] for an identifier with value [text] starting at
+ /// [start].
+ ///
+ /// The [end] location is determined by adding [text] to [start].
+ SourceMapSpan.identifier(SourceLocation start, String text)
+ : this(
+ start,
+ SourceLocation(start.offset + text.length,
+ sourceUrl: start.sourceUrl,
+ line: start.line,
+ column: start.column + text.length),
+ text,
+ isIdentifier: true);
+}
+
+/// A wrapper aruond a [FileSpan] that implements [SourceMapSpan].
+class SourceMapFileSpan implements SourceMapSpan, FileSpan {
+ final FileSpan _inner;
+ @override
+ final bool isIdentifier;
+
+ @override
+ SourceFile get file => _inner.file;
+ @override
+ FileLocation get start => _inner.start;
+ @override
+ FileLocation get end => _inner.end;
+ @override
+ String get text => _inner.text;
+ @override
+ String get context => _inner.context;
+ @override
+ Uri? get sourceUrl => _inner.sourceUrl;
+ @override
+ int get length => _inner.length;
+
+ SourceMapFileSpan(this._inner, {this.isIdentifier = false});
+
+ @override
+ int compareTo(SourceSpan other) => _inner.compareTo(other);
+ @override
+ String highlight({Object? color}) => _inner.highlight(color: color);
+ @override
+ SourceSpan union(SourceSpan other) => _inner.union(other);
+ @override
+ FileSpan expand(FileSpan other) => _inner.expand(other);
+ @override
+ String message(String message, {Object? color}) =>
+ _inner.message(message, color: color);
+ @override
+ String toString() =>
+ _inner.toString().replaceAll('FileSpan', 'SourceMapFileSpan');
+}
diff --git a/pkgs/source_maps/lib/src/utils.dart b/pkgs/source_maps/lib/src/utils.dart
new file mode 100644
index 0000000..f70531e
--- /dev/null
+++ b/pkgs/source_maps/lib/src/utils.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Utilities that shouldn't be in this package.
+library source_maps.utils;
+
+/// Find the first entry in a sorted [list] that matches a monotonic predicate.
+/// Given a result `n`, that all items before `n` will not match, `n` matches,
+/// and all items after `n` match too. The result is -1 when there are no
+/// items, 0 when all items match, and list.length when none does.
+// TODO(sigmund): remove this function after dartbug.com/5624 is fixed.
+int binarySearch<T>(List<T> list, bool Function(T) matches) {
+ if (list.isEmpty) return -1;
+ if (matches(list.first)) return 0;
+ if (!matches(list.last)) return list.length;
+
+ var min = 0;
+ var max = list.length - 1;
+ while (min < max) {
+ var half = min + ((max - min) ~/ 2);
+ if (matches(list[half])) {
+ max = half;
+ } else {
+ min = half + 1;
+ }
+ }
+ return max;
+}
+
+const int lineFeed = 10;
+const int carriageReturn = 13;
diff --git a/pkgs/source_maps/lib/src/vlq.dart b/pkgs/source_maps/lib/src/vlq.dart
new file mode 100644
index 0000000..61b4768
--- /dev/null
+++ b/pkgs/source_maps/lib/src/vlq.dart
@@ -0,0 +1,101 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Utilities to encode and decode VLQ values used in source maps.
+///
+/// Sourcemaps are encoded with variable length numbers as base64 encoded
+/// strings with the least significant digit coming first. Each base64 digit
+/// encodes a 5-bit value (0-31) and a continuation bit. Signed values can be
+/// represented by using the least significant bit of the value as the sign bit.
+///
+/// For more details see the source map [version 3 documentation](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?usp=sharing).
+library source_maps.src.vlq;
+
+import 'dart:math';
+
+const int vlqBaseShift = 5;
+
+const int vlqBaseMask = (1 << 5) - 1;
+
+const int vlqContinuationBit = 1 << 5;
+
+const int vlqContinuationMask = 1 << 5;
+
+const String base64Digits =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+
+final Map<String, int> _digits = () {
+ var map = <String, int>{};
+ for (var i = 0; i < 64; i++) {
+ map[base64Digits[i]] = i;
+ }
+ return map;
+}();
+
+final int maxInt32 = (pow(2, 31) as int) - 1;
+final int minInt32 = -(pow(2, 31) as int);
+
+/// Creates the VLQ encoding of [value] as a sequence of characters
+Iterable<String> encodeVlq(int value) {
+ if (value < minInt32 || value > maxInt32) {
+ throw ArgumentError('expected 32 bit int, got: $value');
+ }
+ var res = <String>[];
+ var signBit = 0;
+ if (value < 0) {
+ signBit = 1;
+ value = -value;
+ }
+ value = (value << 1) | signBit;
+ do {
+ var digit = value & vlqBaseMask;
+ value >>= vlqBaseShift;
+ if (value > 0) {
+ digit |= vlqContinuationBit;
+ }
+ res.add(base64Digits[digit]);
+ } while (value > 0);
+ return res;
+}
+
+/// Decodes a value written as a sequence of VLQ characters. The first input
+/// character will be `chars.current` after calling `chars.moveNext` once. The
+/// iterator is advanced until a stop character is found (a character without
+/// the [vlqContinuationBit]).
+int decodeVlq(Iterator<String> chars) {
+ var result = 0;
+ var stop = false;
+ var shift = 0;
+ while (!stop) {
+ if (!chars.moveNext()) throw StateError('incomplete VLQ value');
+ var char = chars.current;
+ var digit = _digits[char];
+ if (digit == null) {
+ throw FormatException('invalid character in VLQ encoding: $char');
+ }
+ stop = (digit & vlqContinuationBit) == 0;
+ digit &= vlqBaseMask;
+ result += digit << shift;
+ shift += vlqBaseShift;
+ }
+
+ // Result uses the least significant bit as a sign bit. We convert it into a
+ // two-complement value. For example,
+ // 2 (10 binary) becomes 1
+ // 3 (11 binary) becomes -1
+ // 4 (100 binary) becomes 2
+ // 5 (101 binary) becomes -2
+ // 6 (110 binary) becomes 3
+ // 7 (111 binary) becomes -3
+ var negate = (result & 1) == 1;
+ result = result >> 1;
+ result = negate ? -result : result;
+
+ // TODO(sigmund): can we detect this earlier?
+ if (result < minInt32 || result > maxInt32) {
+ throw FormatException(
+ 'expected an encoded 32 bit int, but we got: $result');
+ }
+ return result;
+}
diff --git a/pkgs/source_maps/pubspec.yaml b/pkgs/source_maps/pubspec.yaml
new file mode 100644
index 0000000..8518fa7
--- /dev/null
+++ b/pkgs/source_maps/pubspec.yaml
@@ -0,0 +1,15 @@
+name: source_maps
+version: 0.10.13
+description: A library to programmatically manipulate source map files.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/source_maps
+
+environment:
+ sdk: ^3.3.0
+
+dependencies:
+ source_span: ^1.8.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^2.0.0
+ term_glyph: ^1.2.0
+ test: ^1.16.0
diff --git a/pkgs/source_maps/test/builder_test.dart b/pkgs/source_maps/test/builder_test.dart
new file mode 100644
index 0000000..4f773e7
--- /dev/null
+++ b/pkgs/source_maps/test/builder_test.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:source_maps/source_maps.dart';
+import 'package:test/test.dart';
+
+import 'common.dart';
+
+void main() {
+ test('builder - with span', () {
+ var map = (SourceMapBuilder()
+ ..addSpan(inputVar1, outputVar1)
+ ..addSpan(inputFunction, outputFunction)
+ ..addSpan(inputVar2, outputVar2)
+ ..addSpan(inputExpr, outputExpr))
+ .build(output.url.toString());
+ expect(map, equals(expectedMap));
+ });
+
+ test('builder - with location', () {
+ var str = (SourceMapBuilder()
+ ..addLocation(inputVar1.start, outputVar1.start, 'longVar1')
+ ..addLocation(inputFunction.start, outputFunction.start, 'longName')
+ ..addLocation(inputVar2.start, outputVar2.start, 'longVar2')
+ ..addLocation(inputExpr.start, outputExpr.start, null))
+ .toJson(output.url.toString());
+ expect(str, jsonEncode(expectedMap));
+ });
+}
diff --git a/pkgs/source_maps/test/common.dart b/pkgs/source_maps/test/common.dart
new file mode 100644
index 0000000..f6139de
--- /dev/null
+++ b/pkgs/source_maps/test/common.dart
@@ -0,0 +1,107 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Common input/output used by builder, parser and end2end tests
+library test.common;
+
+import 'package:source_maps/source_maps.dart';
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+/// Content of the source file
+const String inputContent = '''
+/** this is a comment. */
+int longVar1 = 3;
+
+// this is a comment too
+int longName(int longVar2) {
+ return longVar1 + longVar2;
+}
+''';
+final input = SourceFile.fromString(inputContent, url: 'input.dart');
+
+/// A span in the input file
+SourceMapSpan ispan(int start, int end, [bool isIdentifier = false]) =>
+ SourceMapFileSpan(input.span(start, end), isIdentifier: isIdentifier);
+
+SourceMapSpan inputVar1 = ispan(30, 38, true);
+SourceMapSpan inputFunction = ispan(74, 82, true);
+SourceMapSpan inputVar2 = ispan(87, 95, true);
+
+SourceMapSpan inputVar1NoSymbol = ispan(30, 38);
+SourceMapSpan inputFunctionNoSymbol = ispan(74, 82);
+SourceMapSpan inputVar2NoSymbol = ispan(87, 95);
+
+SourceMapSpan inputExpr = ispan(108, 127);
+
+/// Content of the target file
+const String outputContent = '''
+var x = 3;
+f(y) => x + y;
+''';
+final output = SourceFile.fromString(outputContent, url: 'output.dart');
+
+/// A span in the output file
+SourceMapSpan ospan(int start, int end, [bool isIdentifier = false]) =>
+ SourceMapFileSpan(output.span(start, end), isIdentifier: isIdentifier);
+
+SourceMapSpan outputVar1 = ospan(4, 5, true);
+SourceMapSpan outputFunction = ospan(11, 12, true);
+SourceMapSpan outputVar2 = ospan(13, 14, true);
+SourceMapSpan outputVar1NoSymbol = ospan(4, 5);
+SourceMapSpan outputFunctionNoSymbol = ospan(11, 12);
+SourceMapSpan outputVar2NoSymbol = ospan(13, 14);
+SourceMapSpan outputExpr = ospan(19, 24);
+
+/// Expected output mapping when recording the following four mappings:
+/// inputVar1 <= outputVar1
+/// inputFunction <= outputFunction
+/// inputVar2 <= outputVar2
+/// inputExpr <= outputExpr
+///
+/// This mapping is stored in the tests so we can independently test the builder
+/// and parser algorithms without relying entirely on end2end tests.
+const Map<String, dynamic> expectedMap = {
+ 'version': 3,
+ 'sourceRoot': '',
+ 'sources': ['input.dart'],
+ 'names': ['longVar1', 'longName', 'longVar2'],
+ 'mappings': 'IACIA;AAGAC,EAAaC,MACR',
+ 'file': 'output.dart'
+};
+
+void check(SourceSpan outputSpan, Mapping mapping, SourceMapSpan inputSpan,
+ bool realOffsets) {
+ var line = outputSpan.start.line;
+ var column = outputSpan.start.column;
+ var files = realOffsets ? {'input.dart': input} : null;
+ var span = mapping.spanFor(line, column, files: files)!;
+ var span2 = mapping.spanForLocation(outputSpan.start, files: files)!;
+
+ // Both mapping APIs are equivalent.
+ expect(span.start.offset, span2.start.offset);
+ expect(span.start.line, span2.start.line);
+ expect(span.start.column, span2.start.column);
+ expect(span.end.offset, span2.end.offset);
+ expect(span.end.line, span2.end.line);
+ expect(span.end.column, span2.end.column);
+
+ // Mapping matches our input location (modulo using real offsets)
+ expect(span.start.line, inputSpan.start.line);
+ expect(span.start.column, inputSpan.start.column);
+ expect(span.sourceUrl, inputSpan.sourceUrl);
+ expect(span.start.offset, realOffsets ? inputSpan.start.offset : 0);
+
+ // Mapping includes the identifier, if any
+ if (inputSpan.isIdentifier) {
+ expect(span.end.line, inputSpan.end.line);
+ expect(span.end.column, inputSpan.end.column);
+ expect(span.end.offset, span.start.offset + inputSpan.text.length);
+ if (realOffsets) expect(span.end.offset, inputSpan.end.offset);
+ } else {
+ expect(span.end.offset, span.start.offset);
+ expect(span.end.line, span.start.line);
+ expect(span.end.column, span.start.column);
+ }
+}
diff --git a/pkgs/source_maps/test/end2end_test.dart b/pkgs/source_maps/test/end2end_test.dart
new file mode 100644
index 0000000..84dd5ba
--- /dev/null
+++ b/pkgs/source_maps/test/end2end_test.dart
@@ -0,0 +1,160 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_maps/source_maps.dart';
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+import 'common.dart';
+
+void main() {
+ test('end-to-end setup', () {
+ expect(inputVar1.text, 'longVar1');
+ expect(inputFunction.text, 'longName');
+ expect(inputVar2.text, 'longVar2');
+ expect(inputVar1NoSymbol.text, 'longVar1');
+ expect(inputFunctionNoSymbol.text, 'longName');
+ expect(inputVar2NoSymbol.text, 'longVar2');
+ expect(inputExpr.text, 'longVar1 + longVar2');
+
+ expect(outputVar1.text, 'x');
+ expect(outputFunction.text, 'f');
+ expect(outputVar2.text, 'y');
+ expect(outputVar1NoSymbol.text, 'x');
+ expect(outputFunctionNoSymbol.text, 'f');
+ expect(outputVar2NoSymbol.text, 'y');
+ expect(outputExpr.text, 'x + y');
+ });
+
+ test('build + parse', () {
+ var map = (SourceMapBuilder()
+ ..addSpan(inputVar1, outputVar1)
+ ..addSpan(inputFunction, outputFunction)
+ ..addSpan(inputVar2, outputVar2)
+ ..addSpan(inputExpr, outputExpr))
+ .build(output.url.toString());
+ var mapping = parseJson(map);
+ check(outputVar1, mapping, inputVar1, false);
+ check(outputVar2, mapping, inputVar2, false);
+ check(outputFunction, mapping, inputFunction, false);
+ check(outputExpr, mapping, inputExpr, false);
+ });
+
+ test('build + parse - no symbols', () {
+ var map = (SourceMapBuilder()
+ ..addSpan(inputVar1NoSymbol, outputVar1NoSymbol)
+ ..addSpan(inputFunctionNoSymbol, outputFunctionNoSymbol)
+ ..addSpan(inputVar2NoSymbol, outputVar2NoSymbol)
+ ..addSpan(inputExpr, outputExpr))
+ .build(output.url.toString());
+ var mapping = parseJson(map);
+ check(outputVar1NoSymbol, mapping, inputVar1NoSymbol, false);
+ check(outputVar2NoSymbol, mapping, inputVar2NoSymbol, false);
+ check(outputFunctionNoSymbol, mapping, inputFunctionNoSymbol, false);
+ check(outputExpr, mapping, inputExpr, false);
+ });
+
+ test('build + parse, repeated entries', () {
+ var map = (SourceMapBuilder()
+ ..addSpan(inputVar1, outputVar1)
+ ..addSpan(inputVar1, outputVar1)
+ ..addSpan(inputFunction, outputFunction)
+ ..addSpan(inputFunction, outputFunction)
+ ..addSpan(inputVar2, outputVar2)
+ ..addSpan(inputVar2, outputVar2)
+ ..addSpan(inputExpr, outputExpr)
+ ..addSpan(inputExpr, outputExpr))
+ .build(output.url.toString());
+ var mapping = parseJson(map);
+ check(outputVar1, mapping, inputVar1, false);
+ check(outputVar2, mapping, inputVar2, false);
+ check(outputFunction, mapping, inputFunction, false);
+ check(outputExpr, mapping, inputExpr, false);
+ });
+
+ test('build + parse - no symbols, repeated entries', () {
+ var map = (SourceMapBuilder()
+ ..addSpan(inputVar1NoSymbol, outputVar1NoSymbol)
+ ..addSpan(inputVar1NoSymbol, outputVar1NoSymbol)
+ ..addSpan(inputFunctionNoSymbol, outputFunctionNoSymbol)
+ ..addSpan(inputFunctionNoSymbol, outputFunctionNoSymbol)
+ ..addSpan(inputVar2NoSymbol, outputVar2NoSymbol)
+ ..addSpan(inputVar2NoSymbol, outputVar2NoSymbol)
+ ..addSpan(inputExpr, outputExpr))
+ .build(output.url.toString());
+ var mapping = parseJson(map);
+ check(outputVar1NoSymbol, mapping, inputVar1NoSymbol, false);
+ check(outputVar2NoSymbol, mapping, inputVar2NoSymbol, false);
+ check(outputFunctionNoSymbol, mapping, inputFunctionNoSymbol, false);
+ check(outputExpr, mapping, inputExpr, false);
+ });
+
+ test('build + parse with file', () {
+ var json = (SourceMapBuilder()
+ ..addSpan(inputVar1, outputVar1)
+ ..addSpan(inputFunction, outputFunction)
+ ..addSpan(inputVar2, outputVar2)
+ ..addSpan(inputExpr, outputExpr))
+ .toJson(output.url.toString());
+ var mapping = parse(json);
+ check(outputVar1, mapping, inputVar1, true);
+ check(outputVar2, mapping, inputVar2, true);
+ check(outputFunction, mapping, inputFunction, true);
+ check(outputExpr, mapping, inputExpr, true);
+ });
+
+ test('printer projecting marks + parse', () {
+ var out = inputContent.replaceAll('long', '_s');
+ var file = SourceFile.fromString(out, url: 'output2.dart');
+ var printer = Printer('output2.dart');
+ printer.mark(ispan(0, 0));
+
+ var segments = inputContent.split('long');
+ expect(segments.length, 6);
+ printer.add(segments[0], projectMarks: true);
+ printer.mark(inputVar1);
+ printer.add('_s');
+ printer.add(segments[1], projectMarks: true);
+ printer.mark(inputFunction);
+ printer.add('_s');
+ printer.add(segments[2], projectMarks: true);
+ printer.mark(inputVar2);
+ printer.add('_s');
+ printer.add(segments[3], projectMarks: true);
+ printer.mark(inputExpr);
+ printer.add('_s');
+ printer.add(segments[4], projectMarks: true);
+ printer.add('_s');
+ printer.add(segments[5], projectMarks: true);
+
+ expect(printer.text, out);
+
+ var mapping = parse(printer.map);
+ void checkHelper(SourceMapSpan inputSpan, int adjustment) {
+ var start = inputSpan.start.offset - adjustment;
+ var end = (inputSpan.end.offset - adjustment) - 2;
+ var span = SourceMapFileSpan(file.span(start, end),
+ isIdentifier: inputSpan.isIdentifier);
+ check(span, mapping, inputSpan, true);
+ }
+
+ checkHelper(inputVar1, 0);
+ checkHelper(inputFunction, 2);
+ checkHelper(inputVar2, 4);
+ checkHelper(inputExpr, 6);
+
+ // We projected correctly lines that have no mappings
+ check(file.span(66, 66), mapping, ispan(45, 45), true);
+ check(file.span(63, 64), mapping, ispan(45, 45), true);
+ check(file.span(68, 68), mapping, ispan(70, 70), true);
+ check(file.span(71, 71), mapping, ispan(70, 70), true);
+
+ // Start of the last line
+ var oOffset = out.length - 2;
+ var iOffset = inputContent.length - 2;
+ check(file.span(oOffset, oOffset), mapping, ispan(iOffset, iOffset), true);
+ check(file.span(oOffset + 1, oOffset + 1), mapping, ispan(iOffset, iOffset),
+ true);
+ });
+}
diff --git a/pkgs/source_maps/test/parser_test.dart b/pkgs/source_maps/test/parser_test.dart
new file mode 100644
index 0000000..6cfe928
--- /dev/null
+++ b/pkgs/source_maps/test/parser_test.dart
@@ -0,0 +1,431 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: inference_failure_on_collection_literal
+// ignore_for_file: inference_failure_on_instance_creation
+
+import 'dart:convert';
+
+import 'package:source_maps/source_maps.dart';
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+import 'common.dart';
+
+const Map<String, dynamic> _mapWithNoSourceLocation = {
+ 'version': 3,
+ 'sourceRoot': '',
+ 'sources': ['input.dart'],
+ 'names': [],
+ 'mappings': 'A',
+ 'file': 'output.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocation = {
+ 'version': 3,
+ 'sourceRoot': '',
+ 'sources': ['input.dart'],
+ 'names': [],
+ 'mappings': 'AAAA',
+ 'file': 'output.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocationAndMissingNames = {
+ 'version': 3,
+ 'sourceRoot': '',
+ 'sources': ['input.dart'],
+ 'mappings': 'AAAA',
+ 'file': 'output.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocationAndName = {
+ 'version': 3,
+ 'sourceRoot': '',
+ 'sources': ['input.dart'],
+ 'names': ['var'],
+ 'mappings': 'AAAAA',
+ 'file': 'output.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocationAndName1 = {
+ 'version': 3,
+ 'sourceRoot': 'pkg/',
+ 'sources': ['input1.dart'],
+ 'names': ['var1'],
+ 'mappings': 'AAAAA',
+ 'file': 'output.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocationAndName2 = {
+ 'version': 3,
+ 'sourceRoot': 'pkg/',
+ 'sources': ['input2.dart'],
+ 'names': ['var2'],
+ 'mappings': 'AAAAA',
+ 'file': 'output2.dart'
+};
+
+const Map<String, dynamic> _mapWithSourceLocationAndName3 = {
+ 'version': 3,
+ 'sourceRoot': 'pkg/',
+ 'sources': ['input3.dart'],
+ 'names': ['var3'],
+ 'mappings': 'AAAAA',
+ 'file': '3/output.dart'
+};
+
+const _sourceMapBundle = [
+ _mapWithSourceLocationAndName1,
+ _mapWithSourceLocationAndName2,
+ _mapWithSourceLocationAndName3,
+];
+
+void main() {
+ test('parse', () {
+ var mapping = parseJson(expectedMap);
+ check(outputVar1, mapping, inputVar1, false);
+ check(outputVar2, mapping, inputVar2, false);
+ check(outputFunction, mapping, inputFunction, false);
+ check(outputExpr, mapping, inputExpr, false);
+ });
+
+ test('parse + json', () {
+ var mapping = parse(jsonEncode(expectedMap));
+ check(outputVar1, mapping, inputVar1, false);
+ check(outputVar2, mapping, inputVar2, false);
+ check(outputFunction, mapping, inputFunction, false);
+ check(outputExpr, mapping, inputExpr, false);
+ });
+
+ test('parse with file', () {
+ var mapping = parseJson(expectedMap);
+ check(outputVar1, mapping, inputVar1, true);
+ check(outputVar2, mapping, inputVar2, true);
+ check(outputFunction, mapping, inputFunction, true);
+ check(outputExpr, mapping, inputExpr, true);
+ });
+
+ test('parse with no source location', () {
+ var map = parse(jsonEncode(_mapWithNoSourceLocation)) as SingleMapping;
+ expect(map.lines.length, 1);
+ expect(map.lines.first.entries.length, 1);
+ var entry = map.lines.first.entries.first;
+
+ expect(entry.column, 0);
+ expect(entry.sourceUrlId, null);
+ expect(entry.sourceColumn, null);
+ expect(entry.sourceLine, null);
+ expect(entry.sourceNameId, null);
+ });
+
+ test('parse with source location and no name', () {
+ var map = parse(jsonEncode(_mapWithSourceLocation)) as SingleMapping;
+ expect(map.lines.length, 1);
+ expect(map.lines.first.entries.length, 1);
+ var entry = map.lines.first.entries.first;
+
+ expect(entry.column, 0);
+ expect(entry.sourceUrlId, 0);
+ expect(entry.sourceColumn, 0);
+ expect(entry.sourceLine, 0);
+ expect(entry.sourceNameId, null);
+ });
+
+ test('parse with source location and missing names entry', () {
+ var map = parse(jsonEncode(_mapWithSourceLocationAndMissingNames))
+ as SingleMapping;
+ expect(map.lines.length, 1);
+ expect(map.lines.first.entries.length, 1);
+ var entry = map.lines.first.entries.first;
+
+ expect(entry.column, 0);
+ expect(entry.sourceUrlId, 0);
+ expect(entry.sourceColumn, 0);
+ expect(entry.sourceLine, 0);
+ expect(entry.sourceNameId, null);
+ });
+
+ test('parse with source location and name', () {
+ var map = parse(jsonEncode(_mapWithSourceLocationAndName)) as SingleMapping;
+ expect(map.lines.length, 1);
+ expect(map.lines.first.entries.length, 1);
+ var entry = map.lines.first.entries.first;
+
+ expect(entry.sourceUrlId, 0);
+ expect(entry.sourceUrlId, 0);
+ expect(entry.sourceColumn, 0);
+ expect(entry.sourceLine, 0);
+ expect(entry.sourceNameId, 0);
+ });
+
+ test('parse with source root', () {
+ var inputMap = Map.from(_mapWithSourceLocation);
+ inputMap['sourceRoot'] = '/pkg/';
+ var mapping = parseJson(inputMap) as SingleMapping;
+ expect(mapping.spanFor(0, 0)?.sourceUrl, Uri.parse('/pkg/input.dart'));
+ expect(
+ mapping
+ .spanForLocation(
+ SourceLocation(0, sourceUrl: Uri.parse('ignored.dart')))
+ ?.sourceUrl,
+ Uri.parse('/pkg/input.dart'));
+
+ var newSourceRoot = '/new/';
+
+ mapping.sourceRoot = newSourceRoot;
+ inputMap['sourceRoot'] = newSourceRoot;
+
+ expect(mapping.toJson(), equals(inputMap));
+ });
+
+ test('parse with map URL', () {
+ var inputMap = Map.from(_mapWithSourceLocation);
+ inputMap['sourceRoot'] = 'pkg/';
+ var mapping = parseJson(inputMap, mapUrl: 'file:///path/to/map');
+ expect(mapping.spanFor(0, 0)?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input.dart'));
+ });
+
+ group('parse with bundle', () {
+ var mapping =
+ parseJsonExtended(_sourceMapBundle, mapUrl: 'file:///path/to/map');
+
+ test('simple', () {
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.file('/path/to/output.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.file('/path/to/output2.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.file('/path/to/3/output.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+
+ expect(
+ mapping.spanFor(0, 0, uri: 'file:///path/to/output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(
+ mapping.spanFor(0, 0, uri: 'file:///path/to/output2.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(
+ mapping
+ .spanFor(0, 0, uri: 'file:///path/to/3/output.dart')
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+ });
+
+ test('package uris', () {
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.parse('package:1/output.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.parse('package:2/output2.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.parse('package:3/output.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+
+ expect(mapping.spanFor(0, 0, uri: 'package:1/output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(mapping.spanFor(0, 0, uri: 'package:2/output2.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(mapping.spanFor(0, 0, uri: 'package:3/output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+ });
+
+ test('unmapped path', () {
+ var span = mapping.spanFor(0, 0, uri: 'unmapped_output.dart')!;
+ expect(span.sourceUrl, Uri.parse('unmapped_output.dart'));
+ expect(span.start.line, equals(0));
+ expect(span.start.column, equals(0));
+
+ span = mapping.spanFor(10, 5, uri: 'unmapped_output.dart')!;
+ expect(span.sourceUrl, Uri.parse('unmapped_output.dart'));
+ expect(span.start.line, equals(10));
+ expect(span.start.column, equals(5));
+ });
+
+ test('missing path', () {
+ expect(() => mapping.spanFor(0, 0), throwsA(anything));
+ });
+
+ test('incomplete paths', () {
+ expect(mapping.spanFor(0, 0, uri: 'output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(mapping.spanFor(0, 0, uri: 'output2.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(mapping.spanFor(0, 0, uri: '3/output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+ });
+
+ test('parseExtended', () {
+ var mapping = parseExtended(jsonEncode(_sourceMapBundle),
+ mapUrl: 'file:///path/to/map');
+
+ expect(mapping.spanFor(0, 0, uri: 'output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(mapping.spanFor(0, 0, uri: 'output2.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(mapping.spanFor(0, 0, uri: '3/output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+ });
+
+ test('build bundle incrementally', () {
+ var mapping = MappingBundle();
+
+ mapping.addMapping(parseJson(_mapWithSourceLocationAndName1,
+ mapUrl: 'file:///path/to/map') as SingleMapping);
+ expect(mapping.spanFor(0, 0, uri: 'output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+
+ expect(mapping.containsMapping('output2.dart'), isFalse);
+ mapping.addMapping(parseJson(_mapWithSourceLocationAndName2,
+ mapUrl: 'file:///path/to/map') as SingleMapping);
+ expect(mapping.containsMapping('output2.dart'), isTrue);
+ expect(mapping.spanFor(0, 0, uri: 'output2.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+
+ expect(mapping.containsMapping('3/output.dart'), isFalse);
+ mapping.addMapping(parseJson(_mapWithSourceLocationAndName3,
+ mapUrl: 'file:///path/to/map') as SingleMapping);
+ expect(mapping.containsMapping('3/output.dart'), isTrue);
+ expect(mapping.spanFor(0, 0, uri: '3/output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+ });
+
+ // Test that the source map can handle cases where the uri passed in is
+ // not from the expected host but it is still unambiguous which source
+ // map should be used.
+ test('different paths', () {
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.parse('http://localhost/output.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.parse('http://localhost/output2.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(
+ mapping
+ .spanForLocation(SourceLocation(0,
+ sourceUrl: Uri.parse('http://localhost/3/output.dart')))
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+
+ expect(
+ mapping.spanFor(0, 0, uri: 'http://localhost/output.dart')?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input1.dart'));
+ expect(
+ mapping
+ .spanFor(0, 0, uri: 'http://localhost/output2.dart')
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input2.dart'));
+ expect(
+ mapping
+ .spanFor(0, 0, uri: 'http://localhost/3/output.dart')
+ ?.sourceUrl,
+ Uri.parse('file:///path/to/pkg/input3.dart'));
+ });
+ });
+
+ test('parse and re-emit', () {
+ for (var expected in [
+ expectedMap,
+ _mapWithNoSourceLocation,
+ _mapWithSourceLocation,
+ _mapWithSourceLocationAndName
+ ]) {
+ var mapping = parseJson(expected) as SingleMapping;
+ expect(mapping.toJson(), equals(expected));
+
+ mapping = parseJsonExtended(expected) as SingleMapping;
+ expect(mapping.toJson(), equals(expected));
+ }
+
+ var mapping = parseJsonExtended(_sourceMapBundle) as MappingBundle;
+ expect(mapping.toJson(), equals(_sourceMapBundle));
+ });
+
+ test('parse extensions', () {
+ var map = Map.from(expectedMap);
+ map['x_foo'] = 'a';
+ map['x_bar'] = [3];
+ var mapping = parseJson(map) as SingleMapping;
+ expect(mapping.toJson(), equals(map));
+ expect(mapping.extensions['x_foo'], equals('a'));
+ expect((mapping.extensions['x_bar'] as List).first, equals(3));
+ });
+
+ group('source files', () {
+ group('from fromEntries()', () {
+ test('are null for non-FileLocations', () {
+ var mapping = SingleMapping.fromEntries([
+ Entry(SourceLocation(10, line: 1, column: 8), outputVar1.start, null)
+ ]);
+ expect(mapping.files, equals([null]));
+ });
+
+ test("use a file location's file", () {
+ var mapping = SingleMapping.fromEntries(
+ [Entry(inputVar1.start, outputVar1.start, null)]);
+ expect(mapping.files, equals([input]));
+ });
+ });
+
+ group('from parse()', () {
+ group('are null', () {
+ test('with no sourcesContent field', () {
+ var mapping = parseJson(expectedMap) as SingleMapping;
+ expect(mapping.files, equals([null]));
+ });
+
+ test('with null sourcesContent values', () {
+ var map = Map.from(expectedMap);
+ map['sourcesContent'] = [null];
+ var mapping = parseJson(map) as SingleMapping;
+ expect(mapping.files, equals([null]));
+ });
+
+ test('with a too-short sourcesContent', () {
+ var map = Map.from(expectedMap);
+ map['sourcesContent'] = [];
+ var mapping = parseJson(map) as SingleMapping;
+ expect(mapping.files, equals([null]));
+ });
+ });
+
+ test('are parsed from sourcesContent', () {
+ var map = Map.from(expectedMap);
+ map['sourcesContent'] = ['hello, world!'];
+ var mapping = parseJson(map) as SingleMapping;
+
+ var file = mapping.files[0]!;
+ expect(file.url, equals(Uri.parse('input.dart')));
+ expect(file.getText(0), equals('hello, world!'));
+ });
+ });
+ });
+}
diff --git a/pkgs/source_maps/test/printer_test.dart b/pkgs/source_maps/test/printer_test.dart
new file mode 100644
index 0000000..89265e3
--- /dev/null
+++ b/pkgs/source_maps/test/printer_test.dart
@@ -0,0 +1,126 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:source_maps/source_maps.dart';
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+import 'common.dart';
+
+void main() {
+ test('printer', () {
+ var printer = Printer('output.dart');
+ printer
+ ..add('var ')
+ ..mark(inputVar1)
+ ..add('x = 3;\n')
+ ..mark(inputFunction)
+ ..add('f(')
+ ..mark(inputVar2)
+ ..add('y) => ')
+ ..mark(inputExpr)
+ ..add('x + y;\n');
+ expect(printer.text, outputContent);
+ expect(printer.map, jsonEncode(expectedMap));
+ });
+
+ test('printer projecting marks', () {
+ var out = inputContent.replaceAll('long', '_s');
+ var printer = Printer('output2.dart');
+
+ var segments = inputContent.split('long');
+ expect(segments.length, 6);
+ printer
+ ..mark(ispan(0, 0))
+ ..add(segments[0], projectMarks: true)
+ ..mark(inputVar1)
+ ..add('_s')
+ ..add(segments[1], projectMarks: true)
+ ..mark(inputFunction)
+ ..add('_s')
+ ..add(segments[2], projectMarks: true)
+ ..mark(inputVar2)
+ ..add('_s')
+ ..add(segments[3], projectMarks: true)
+ ..mark(inputExpr)
+ ..add('_s')
+ ..add(segments[4], projectMarks: true)
+ ..add('_s')
+ ..add(segments[5], projectMarks: true);
+
+ expect(printer.text, out);
+ // 8 new lines in the source map:
+ expect(printer.map.split(';').length, 8);
+
+ SourceMapSpan asFixed(SourceMapSpan s) =>
+ SourceMapSpan(s.start, s.end, s.text, isIdentifier: s.isIdentifier);
+
+ // The result is the same if we use fixed positions
+ var printer2 = Printer('output2.dart');
+ printer2
+ ..mark(SourceLocation(0, sourceUrl: 'input.dart').pointSpan())
+ ..add(segments[0], projectMarks: true)
+ ..mark(asFixed(inputVar1))
+ ..add('_s')
+ ..add(segments[1], projectMarks: true)
+ ..mark(asFixed(inputFunction))
+ ..add('_s')
+ ..add(segments[2], projectMarks: true)
+ ..mark(asFixed(inputVar2))
+ ..add('_s')
+ ..add(segments[3], projectMarks: true)
+ ..mark(asFixed(inputExpr))
+ ..add('_s')
+ ..add(segments[4], projectMarks: true)
+ ..add('_s')
+ ..add(segments[5], projectMarks: true);
+
+ expect(printer2.text, out);
+ expect(printer2.map, printer.map);
+ });
+
+ group('nested printer', () {
+ test('simple use', () {
+ var printer = NestedPrinter();
+ printer
+ ..add('var ')
+ ..add('x = 3;\n', span: inputVar1)
+ ..add('f(', span: inputFunction)
+ ..add('y) => ', span: inputVar2)
+ ..add('x + y;\n', span: inputExpr)
+ ..build('output.dart');
+ expect(printer.text, outputContent);
+ expect(printer.map, jsonEncode(expectedMap));
+ });
+
+ test('nested use', () {
+ var printer = NestedPrinter();
+ printer
+ ..add('var ')
+ ..add(NestedPrinter()..add('x = 3;\n', span: inputVar1))
+ ..add('f(', span: inputFunction)
+ ..add(NestedPrinter()..add('y) => ', span: inputVar2))
+ ..add('x + y;\n', span: inputExpr)
+ ..build('output.dart');
+ expect(printer.text, outputContent);
+ expect(printer.map, jsonEncode(expectedMap));
+ });
+
+ test('add indentation', () {
+ var out = inputContent.replaceAll('long', '_s');
+ var lines = inputContent.trim().split('\n');
+ expect(lines.length, 7);
+ var printer = NestedPrinter();
+ for (var i = 0; i < lines.length; i++) {
+ if (i == 5) printer.indent++;
+ printer.addLine(lines[i].replaceAll('long', '_s').trim());
+ if (i == 5) printer.indent--;
+ }
+ printer.build('output.dart');
+ expect(printer.text, out);
+ });
+ });
+}
diff --git a/pkgs/source_maps/test/refactor_test.dart b/pkgs/source_maps/test/refactor_test.dart
new file mode 100644
index 0000000..5bc3818
--- /dev/null
+++ b/pkgs/source_maps/test/refactor_test.dart
@@ -0,0 +1,199 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_maps/parser.dart' show Mapping, parse;
+import 'package:source_maps/refactor.dart';
+import 'package:source_span/source_span.dart';
+import 'package:term_glyph/term_glyph.dart' as term_glyph;
+import 'package:test/test.dart';
+
+void main() {
+ setUpAll(() {
+ term_glyph.ascii = true;
+ });
+
+ group('conflict detection', () {
+ var original = '0123456789abcdefghij';
+ var file = SourceFile.fromString(original);
+
+ test('no conflict, in order', () {
+ var txn = TextEditTransaction(original, file);
+ txn.edit(2, 4, '.');
+ txn.edit(5, 5, '|');
+ txn.edit(6, 6, '-');
+ txn.edit(6, 7, '_');
+ expect((txn.commit()..build('')).text, '01.4|5-_789abcdefghij');
+ });
+
+ test('no conflict, out of order', () {
+ var txn = TextEditTransaction(original, file);
+ txn.edit(2, 4, '.');
+ txn.edit(5, 5, '|');
+
+ // Regresion test for issue #404: there is no conflict/overlap for edits
+ // that don't remove any of the original code.
+ txn.edit(6, 7, '_');
+ txn.edit(6, 6, '-');
+ expect((txn.commit()..build('')).text, '01.4|5-_789abcdefghij');
+ });
+
+ test('conflict', () {
+ var txn = TextEditTransaction(original, file);
+ txn.edit(2, 4, '.');
+ txn.edit(3, 3, '-');
+ expect(
+ () => txn.commit(),
+ throwsA(
+ predicate((e) => e.toString().contains('overlapping edits'))));
+ });
+ });
+
+ test('generated source maps', () {
+ var original =
+ '0123456789\n0*23456789\n01*3456789\nabcdefghij\nabcd*fghij\n';
+ var file = SourceFile.fromString(original);
+ var txn = TextEditTransaction(original, file);
+ txn.edit(27, 29, '__\n ');
+ txn.edit(34, 35, '___');
+ var printer = (txn.commit()..build(''));
+ var output = printer.text;
+ var map = parse(printer.map!);
+ expect(output,
+ '0123456789\n0*23456789\n01*34__\n 789\na___cdefghij\nabcd*fghij\n');
+
+ // Line 1 and 2 are unmodified: mapping any column returns the beginning
+ // of the corresponding line:
+ expect(
+ _span(1, 1, map, file),
+ 'line 1, column 1: \n'
+ ' ,\n'
+ '1 | 0123456789\n'
+ ' | ^\n'
+ " '");
+ expect(
+ _span(1, 5, map, file),
+ 'line 1, column 1: \n'
+ ' ,\n'
+ '1 | 0123456789\n'
+ ' | ^\n'
+ " '");
+ expect(
+ _span(2, 1, map, file),
+ 'line 2, column 1: \n'
+ ' ,\n'
+ '2 | 0*23456789\n'
+ ' | ^\n'
+ " '");
+ expect(
+ _span(2, 8, map, file),
+ 'line 2, column 1: \n'
+ ' ,\n'
+ '2 | 0*23456789\n'
+ ' | ^\n'
+ " '");
+
+ // Line 3 is modified part way: mappings before the edits have the right
+ // mapping, after the edits the mapping is null.
+ expect(
+ _span(3, 1, map, file),
+ 'line 3, column 1: \n'
+ ' ,\n'
+ '3 | 01*3456789\n'
+ ' | ^\n'
+ " '");
+ expect(
+ _span(3, 5, map, file),
+ 'line 3, column 1: \n'
+ ' ,\n'
+ '3 | 01*3456789\n'
+ ' | ^\n'
+ " '");
+
+ // Start of edits map to beginning of the edit secion:
+ expect(
+ _span(3, 6, map, file),
+ 'line 3, column 6: \n'
+ ' ,\n'
+ '3 | 01*3456789\n'
+ ' | ^\n'
+ " '");
+ expect(
+ _span(3, 7, map, file),
+ 'line 3, column 6: \n'
+ ' ,\n'
+ '3 | 01*3456789\n'
+ ' | ^\n'
+ " '");
+
+ // Lines added have no mapping (they should inherit the last mapping),
+ // but the end of the edit region continues were we left off:
+ expect(_span(4, 1, map, file), isNull);
+ expect(
+ _span(4, 5, map, file),
+ 'line 3, column 8: \n'
+ ' ,\n'
+ '3 | 01*3456789\n'
+ ' | ^\n'
+ " '");
+
+ // Subsequent lines are still mapped correctly:
+ // a (in a___cd...)
+ expect(
+ _span(5, 1, map, file),
+ 'line 4, column 1: \n'
+ ' ,\n'
+ '4 | abcdefghij\n'
+ ' | ^\n'
+ " '");
+ // _ (in a___cd...)
+ expect(
+ _span(5, 2, map, file),
+ 'line 4, column 2: \n'
+ ' ,\n'
+ '4 | abcdefghij\n'
+ ' | ^\n'
+ " '");
+ // _ (in a___cd...)
+ expect(
+ _span(5, 3, map, file),
+ 'line 4, column 2: \n'
+ ' ,\n'
+ '4 | abcdefghij\n'
+ ' | ^\n'
+ " '");
+ // _ (in a___cd...)
+ expect(
+ _span(5, 4, map, file),
+ 'line 4, column 2: \n'
+ ' ,\n'
+ '4 | abcdefghij\n'
+ ' | ^\n'
+ " '");
+ // c (in a___cd...)
+ expect(
+ _span(5, 5, map, file),
+ 'line 4, column 3: \n'
+ ' ,\n'
+ '4 | abcdefghij\n'
+ ' | ^\n'
+ " '");
+ expect(
+ _span(6, 1, map, file),
+ 'line 5, column 1: \n'
+ ' ,\n'
+ '5 | abcd*fghij\n'
+ ' | ^\n'
+ " '");
+ expect(
+ _span(6, 8, map, file),
+ 'line 5, column 1: \n'
+ ' ,\n'
+ '5 | abcd*fghij\n'
+ ' | ^\n'
+ " '");
+ });
+}
+
+String? _span(int line, int column, Mapping map, SourceFile file) =>
+ map.spanFor(line - 1, column - 1, files: {'': file})?.message('').trim();
diff --git a/pkgs/source_maps/test/utils_test.dart b/pkgs/source_maps/test/utils_test.dart
new file mode 100644
index 0000000..4abdce2
--- /dev/null
+++ b/pkgs/source_maps/test/utils_test.dart
@@ -0,0 +1,53 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Tests for the binary search utility algorithm.
+library test.utils_test;
+
+import 'package:source_maps/src/utils.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('binary search', () {
+ test('empty', () {
+ expect(binarySearch([], (x) => true), -1);
+ });
+
+ test('single element', () {
+ expect(binarySearch([1], (x) => true), 0);
+ expect(binarySearch([1], (x) => false), 1);
+ });
+
+ test('no matches', () {
+ var list = [1, 2, 3, 4, 5, 6, 7];
+ expect(binarySearch(list, (x) => false), list.length);
+ });
+
+ test('all match', () {
+ var list = [1, 2, 3, 4, 5, 6, 7];
+ expect(binarySearch(list, (x) => true), 0);
+ });
+
+ test('compare with linear search', () {
+ for (var size = 0; size < 100; size++) {
+ var list = <int>[];
+ for (var i = 0; i < size; i++) {
+ list.add(i);
+ }
+ for (var pos = 0; pos <= size; pos++) {
+ expect(binarySearch(list, (x) => x >= pos),
+ _linearSearch(list, (x) => x >= pos));
+ }
+ }
+ });
+ });
+}
+
+int _linearSearch<T>(List<T> list, bool Function(T) predicate) {
+ if (list.isEmpty) return -1;
+ for (var i = 0; i < list.length; i++) {
+ if (predicate(list[i])) return i;
+ }
+ return list.length;
+}
diff --git a/pkgs/source_maps/test/vlq_test.dart b/pkgs/source_maps/test/vlq_test.dart
new file mode 100644
index 0000000..4568cff
--- /dev/null
+++ b/pkgs/source_maps/test/vlq_test.dart
@@ -0,0 +1,59 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:math';
+
+import 'package:source_maps/src/vlq.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('encode and decode - simple values', () {
+ expect(encodeVlq(1).join(''), 'C');
+ expect(encodeVlq(2).join(''), 'E');
+ expect(encodeVlq(3).join(''), 'G');
+ expect(encodeVlq(100).join(''), 'oG');
+ expect(decodeVlq('C'.split('').iterator), 1);
+ expect(decodeVlq('E'.split('').iterator), 2);
+ expect(decodeVlq('G'.split('').iterator), 3);
+ expect(decodeVlq('oG'.split('').iterator), 100);
+ });
+
+ test('encode and decode', () {
+ for (var i = -10000; i < 10000; i++) {
+ _checkEncodeDecode(i);
+ }
+ });
+
+ test('only 32-bit ints allowed', () {
+ var maxInt = (pow(2, 31) as int) - 1;
+ var minInt = -(pow(2, 31) as int);
+ _checkEncodeDecode(maxInt - 1);
+ _checkEncodeDecode(minInt + 1);
+ _checkEncodeDecode(maxInt);
+ _checkEncodeDecode(minInt);
+
+ expect(encodeVlq(minInt).join(''), 'hgggggE');
+ expect(decodeVlq('hgggggE'.split('').iterator), minInt);
+
+ expect(() => encodeVlq(maxInt + 1), throwsA(anything));
+ expect(() => encodeVlq(maxInt + 2), throwsA(anything));
+ expect(() => encodeVlq(minInt - 1), throwsA(anything));
+ expect(() => encodeVlq(minInt - 2), throwsA(anything));
+
+ // if we allowed more than 32 bits, these would be the expected encodings
+ // for the large numbers above.
+ expect(() => decodeVlq('ggggggE'.split('').iterator), throwsA(anything));
+ expect(() => decodeVlq('igggggE'.split('').iterator), throwsA(anything));
+ expect(() => decodeVlq('jgggggE'.split('').iterator), throwsA(anything));
+ expect(() => decodeVlq('lgggggE'.split('').iterator), throwsA(anything));
+ },
+ // This test uses integers so large they overflow in JS.
+ testOn: 'dart-vm');
+}
+
+void _checkEncodeDecode(int value) {
+ var encoded = encodeVlq(value);
+ expect(decodeVlq(encoded.iterator), value);
+ expect(decodeVlq(encoded.join('').split('').iterator), value);
+}
diff --git a/pkgs/source_span/.gitignore b/pkgs/source_span/.gitignore
new file mode 100644
index 0000000..ab3cb76
--- /dev/null
+++ b/pkgs/source_span/.gitignore
@@ -0,0 +1,16 @@
+# Don’t commit the following directories created by pub.
+.buildlog
+.dart_tool/
+.pub/
+build/
+packages
+.packages
+
+# Or the files created by dart2js.
+*.dart.js
+*.js_
+*.js.deps
+*.js.map
+
+# Include when developing application packages.
+pubspec.lock
diff --git a/pkgs/source_span/CHANGELOG.md b/pkgs/source_span/CHANGELOG.md
new file mode 100644
index 0000000..b8319d7
--- /dev/null
+++ b/pkgs/source_span/CHANGELOG.md
@@ -0,0 +1,240 @@
+## 1.10.1
+
+* Require Dart 3.1
+* Move to `dart-lang/tools` monorepo.
+
+## 1.10.0
+
+* Add a `SourceFile.codeUnits` property.
+* Require Dart 2.18
+* Add an API usage example in `example/`.
+
+## 1.9.1
+
+* Properly handle multi-line labels for multi-span highlights.
+
+* Populate the pubspec `repository` field.
+
+## 1.9.0
+
+* Add `SourceSpanWithContextExtension.subspan` that returns a
+ `SourceSpanWithContext` rather than a plain `SourceSpan`.
+
+## 1.8.2
+
+* Fix a bug where highlighting multiple spans with `null` URLs could cause an
+ assertion error. Now when multiple spans are passed with `null` URLs, they're
+ highlighted as though they all come from different source files.
+
+## 1.8.1
+
+* Fix a bug where the URL header for the highlights with multiple files would
+ get omitted only one span has a non-null URI.
+
+## 1.8.0
+
+* Stable release for null safety.
+
+## 1.7.0
+
+* Add a `SourceSpan.subspan()` extension method which returns a slice of an
+ existing source span.
+
+## 1.6.0
+
+* Add support for highlighting multiple source spans at once, providing more
+ context for span-based messages. This is exposed through the new APIs
+ `SourceSpan.highlightMultiple()` and `SourceSpan.messageMultiple()` (both
+ extension methods), `MultiSourceSpanException`, and
+ `MultiSourceSpanFormatException`.
+
+## 1.5.6
+
+* Fix padding around line numbers that are powers of 10 in
+ `FileSpan.highlight()`.
+
+## 1.5.5
+
+* Fix a bug where `FileSpan.highlight()` would crash for spans that covered a
+ trailing newline and a single additional empty line.
+
+## 1.5.4
+
+* `FileSpan.highlight()` now properly highlights point spans at the beginning of
+ lines.
+
+## 1.5.3
+
+* Fix an edge case where `FileSpan.highlight()` would put the highlight
+ indicator in the wrong position when highlighting a point span after the end
+ of a file.
+
+## 1.5.2
+
+* `SourceFile.span()` now goes to the end of the file by default, rather than
+ ending one character before the end of the file. This matches the documented
+ behavior.
+
+* `FileSpan.context` now includes the full line on which the span appears for
+ empty spans at the beginning and end of lines.
+
+* Fix an edge case where `FileSpan.highlight()` could crash when highlighting a
+ span that ended with an empty line.
+
+## 1.5.1
+
+* Produce better source span highlights for multi-line spans that cover the
+ entire last line of the span, including the newline.
+
+* Produce better source span highlights for spans that contain Windows-style
+ newlines.
+
+## 1.5.0
+
+* Improve the output of `SourceSpan.highlight()` and `SourceSpan.message()`:
+
+ * They now include line numbers.
+ * They will now print every line of a multiline span.
+ * They will now use Unicode box-drawing characters by default (this can be
+ controlled using [`term_glyph.ascii`][]).
+
+[`term_glyph.ascii`]: https://pub.dartlang.org/documentation/term_glyph/latest/term_glyph/ascii.html
+
+## 1.4.1
+
+* Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 1.4.0
+
+* The `new SourceFile()` constructor is deprecated. This constructed a source
+ file from a string's runes, rather than its code units, which runs counter to
+ the way Dart handles strings otherwise. The `new StringFile.fromString()`
+ constructor (see below) should be used instead.
+
+* The `new SourceFile.fromString()` constructor was added. This works like `new
+ SourceFile()`, except it uses code units rather than runes.
+
+* The current behavior when characters larger than `0xFFFF` are passed to `new
+ SourceFile.decoded()` is now considered deprecated.
+
+## 1.3.1
+
+* Properly highlight spans for lines that include tabs with
+ `SourceSpan.highlight()` and `SourceSpan.message()`.
+
+## 1.3.0
+
+* Add `SourceSpan.highlight()`, which returns just the highlighted text that
+ would be included in `SourceSpan.message()`.
+
+## 1.2.4
+
+* Fix a new strong mode error.
+
+## 1.2.3
+
+* Fix a bug where a point span at the end of a file without a trailing newline
+ would be printed incorrectly.
+
+## 1.2.2
+
+* Allow `SourceSpanException.message`, `SourceSpanFormatException.source`, and
+ `SourceSpanWithContext.context` to be overridden in strong mode.
+
+## 1.2.1
+
+* Fix the declared type of `FileSpan.start` and `FileSpan.end`. In 1.2.0 these
+ were mistakenly changed from `FileLocation` to `SourceLocation`.
+
+## 1.2.0
+
+* **Deprecated:** Extending `SourceLocation` directly is deprecated. Instead,
+ extend the new `SourceLocationBase` class or mix in the new
+ `SourceLocationMixin` mixin.
+
+* Dramatically improve the performance of `FileLocation`.
+
+## 1.1.6
+
+* Optimize `getLine()` in `SourceFile` when repeatedly called.
+
+## 1.1.5
+
+* Fixed another case in which `FileSpan.union` could throw an exception for
+ external implementations of `FileSpan`.
+
+## 1.1.4
+
+* Eliminated dart2js warning about overriding `==`, but not `hashCode`.
+
+## 1.1.3
+
+* `FileSpan.compareTo`, `FileSpan.==`, `FileSpan.union`, and `FileSpan.expand`
+ no longer throw exceptions for external implementations of `FileSpan`.
+
+* `FileSpan.hashCode` now fully agrees with `FileSpan.==`.
+
+## 1.1.2
+
+* Fixed validation in `SourceSpanWithContext` to allow multiple occurrences of
+ `text` within `context`.
+
+## 1.1.1
+
+* Fixed `FileSpan`'s context to include the full span text, not just the first
+ line of it.
+
+## 1.1.0
+
+* Added `SourceSpanWithContext`: a span that also includes the full line of text
+ that contains the span.
+
+## 1.0.3
+
+* Cleanup equality operator to accept any Object rather than just a
+ `SourceLocation`.
+
+## 1.0.2
+
+* Avoid unintentionally allocating extra objects for internal `FileSpan`
+ operations.
+
+* Ensure that `SourceSpan.operator==` works on arbitrary `Object`s.
+
+## 1.0.1
+
+* Use a more compact internal representation for `FileSpan`.
+
+## 1.0.0
+
+This package was extracted from the
+[`source_maps`](https://pub.dev/packages/source_maps) package, but the
+API has many differences. Among them:
+
+* `Span` has been renamed to `SourceSpan` and `Location` has been renamed to
+ `SourceLocation` to clarify their purpose and maintain consistency with the
+ package name. Likewise, `SpanException` is now `SourceSpanException` and
+ `SpanFormatException` is not `SourceSpanFormatException`.
+
+* `FixedSpan` and `FixedLocation` have been rolled into the `Span` and
+ `Location` classes, respectively.
+
+* `SourceFile` is more aggressive about validating its arguments. Out-of-bounds
+ lines, columns, and offsets will now throw errors rather than be silently
+ clamped.
+
+* `SourceSpan.sourceUrl`, `SourceLocation.sourceUrl`, and `SourceFile.url` now
+ return `Uri` objects rather than `String`s. The constructors allow either
+ `String`s or `Uri`s.
+
+* `Span.getLocationMessage` and `SourceFile.getLocationMessage` are now
+ `SourceSpan.message` and `SourceFile.message`, respectively. Rather than
+ taking both a `useColor` and a `color` parameter, they now take a single
+ `color` parameter that controls both whether and which color is used.
+
+* `Span.isIdentifier` has been removed. This property doesn't make sense outside
+ of a source map context.
+
+* `SourceFileSegment` has been removed. This class wasn't widely used and was
+ inconsistent in its choice of which parameters were considered relative and
+ which absolute.
diff --git a/pkgs/source_span/LICENSE b/pkgs/source_span/LICENSE
new file mode 100644
index 0000000..000cd7b
--- /dev/null
+++ b/pkgs/source_span/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/source_span/README.md b/pkgs/source_span/README.md
new file mode 100644
index 0000000..b4ce25f
--- /dev/null
+++ b/pkgs/source_span/README.md
@@ -0,0 +1,21 @@
+[](https://github.com/dart-lang/tools/actions/workflows/source_span.yaml)
+[](https://pub.dev/packages/source_span)
+[](https://pub.dev/packages/source_span/publisher)
+
+## About this package
+
+`source_span` is a library for tracking locations in source code. It's designed
+to provide a standard representation for source code locations and spans so that
+disparate packages can easily pass them among one another, and to make it easy
+to generate human-friendly messages associated with a given piece of code.
+
+The most commonly-used class is the package's namesake, `SourceSpan`. It
+represents a span of characters in some source file, and is often attached to an
+object that has been parsed to indicate where it was parsed from. It provides
+access to the text of the span via `SourceSpan.text` and can be used to produce
+human-friendly messages using `SourceSpan.message()`.
+
+When parsing code from a file, `SourceFile` is useful. Not only does it provide
+an efficient means of computing line and column numbers, `SourceFile.span()`
+returns special `FileSpan`s that are able to provide more context for their
+error messages.
diff --git a/pkgs/source_span/analysis_options.yaml b/pkgs/source_span/analysis_options.yaml
new file mode 100644
index 0000000..d2ebdbf
--- /dev/null
+++ b/pkgs/source_span/analysis_options.yaml
@@ -0,0 +1,32 @@
+# https://dart.dev/guides/language/analysis-options
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
+ strict-inference: true
+ strict-raw-types: true
+
+linter:
+ rules:
+ - avoid_bool_literals_in_conditional_expressions
+ - avoid_classes_with_only_static_members
+ - avoid_private_typedef_functions
+ - avoid_redundant_argument_values
+ - avoid_returning_this
+ - avoid_unused_constructor_parameters
+ - avoid_void_async
+ - cancel_subscriptions
+ - cascade_invocations
+ - join_return_with_assignment
+ - literal_only_boolean_expressions
+ - missing_whitespace_between_adjacent_strings
+ - no_adjacent_strings_in_list
+ - prefer_const_declarations
+ - prefer_expression_function_bodies
+ - prefer_final_locals
+ - unnecessary_await_in_return
+ - unnecessary_raw_strings
+ - use_if_null_to_convert_nulls_to_bools
+ - use_raw_strings
+ - use_string_buffers
diff --git a/pkgs/source_span/example/main.dart b/pkgs/source_span/example/main.dart
new file mode 100644
index 0000000..e296765
--- /dev/null
+++ b/pkgs/source_span/example/main.dart
@@ -0,0 +1,51 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:source_span/source_span.dart';
+
+void main(List<String> args) {
+ final file = File('README.md');
+ final contents = file.readAsStringSync();
+
+ final sourceFile = SourceFile.fromString(contents, url: file.uri);
+ final spans = _parseFile(contents, sourceFile);
+
+ for (var span in spans.take(30)) {
+ print('[${span.start.line + 1}:${span.start.column + 1}] ${span.text}');
+ }
+}
+
+Iterable<SourceSpan> _parseFile(String contents, SourceFile sourceFile) sync* {
+ var wordStart = 0;
+ var inWhiteSpace = true;
+
+ for (var i = 0; i < contents.length; i++) {
+ final codeUnit = contents.codeUnitAt(i);
+
+ if (codeUnit == _eol || codeUnit == _space) {
+ if (!inWhiteSpace) {
+ inWhiteSpace = true;
+
+ // emit a word
+ yield sourceFile.span(wordStart, i);
+ }
+ } else {
+ if (inWhiteSpace) {
+ inWhiteSpace = false;
+
+ wordStart = i;
+ }
+ }
+ }
+
+ if (!inWhiteSpace) {
+ // emit a word
+ yield sourceFile.span(wordStart, contents.length);
+ }
+}
+
+const int _eol = 10;
+const int _space = 32;
diff --git a/pkgs/source_span/lib/source_span.dart b/pkgs/source_span/lib/source_span.dart
new file mode 100644
index 0000000..534a3a7
--- /dev/null
+++ b/pkgs/source_span/lib/source_span.dart
@@ -0,0 +1,11 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'src/file.dart';
+export 'src/location.dart';
+export 'src/location_mixin.dart';
+export 'src/span.dart';
+export 'src/span_exception.dart';
+export 'src/span_mixin.dart';
+export 'src/span_with_context.dart';
diff --git a/pkgs/source_span/lib/src/charcode.dart b/pkgs/source_span/lib/src/charcode.dart
new file mode 100644
index 0000000..5182638
--- /dev/null
+++ b/pkgs/source_span/lib/src/charcode.dart
@@ -0,0 +1,15 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// "Carriage return" control character.
+const int $cr = 0x0D;
+
+/// "Line feed" control character.
+const int $lf = 0x0A;
+
+/// Space character.
+const int $space = 0x20;
+
+/// "Horizontal Tab" control character, common name.
+const int $tab = 0x09;
diff --git a/pkgs/source_span/lib/src/colors.dart b/pkgs/source_span/lib/src/colors.dart
new file mode 100644
index 0000000..b48d468
--- /dev/null
+++ b/pkgs/source_span/lib/src/colors.dart
@@ -0,0 +1,12 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Color constants used for generating messages.
+const String red = '\u001b[31m';
+
+const String yellow = '\u001b[33m';
+
+const String blue = '\u001b[34m';
+
+const String none = '\u001b[0m';
diff --git a/pkgs/source_span/lib/src/file.dart b/pkgs/source_span/lib/src/file.dart
new file mode 100644
index 0000000..74c9234
--- /dev/null
+++ b/pkgs/source_span/lib/src/file.dart
@@ -0,0 +1,454 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:math' as math;
+import 'dart:typed_data';
+
+import 'location.dart';
+import 'location_mixin.dart';
+import 'span.dart';
+import 'span_mixin.dart';
+import 'span_with_context.dart';
+
+// Constants to determine end-of-lines.
+const int _lf = 10;
+const int _cr = 13;
+
+/// A class representing a source file.
+///
+/// This doesn't necessarily have to correspond to a file on disk, just a chunk
+/// of text usually with a URL associated with it.
+class SourceFile {
+ /// The URL where the source file is located.
+ ///
+ /// This may be null, indicating that the URL is unknown or unavailable.
+ final Uri? url;
+
+ /// An array of offsets for each line beginning in the file.
+ ///
+ /// Each offset refers to the first character *after* the newline. If the
+ /// source file has a trailing newline, the final offset won't actually be in
+ /// the file.
+ final _lineStarts = <int>[0];
+
+ /// The code units of the characters in the file.
+ ///
+ /// If this was constructed with the deprecated `SourceFile()` constructor,
+ /// this will instead contain the code _points_ of the characters in the file
+ /// (so characters above 2^16 are represented as individual integers rather
+ /// than surrogate pairs).
+ List<int> get codeUnits => _decodedChars;
+
+ /// The code units of the characters in this file.
+ final Uint32List _decodedChars;
+
+ /// The length of the file in characters.
+ int get length => _decodedChars.length;
+
+ /// The number of lines in the file.
+ int get lines => _lineStarts.length;
+
+ /// The line that the offset fell on the last time [getLine] was called.
+ ///
+ /// In many cases, sequential calls to getLine() are for nearby, usually
+ /// increasing offsets. In that case, we can find the line for an offset
+ /// quickly by first checking to see if the offset is on the same line as the
+ /// previous result.
+ int? _cachedLine;
+
+ /// This constructor is deprecated.
+ ///
+ /// Use [SourceFile.fromString] instead.
+ @Deprecated('Will be removed in 2.0.0')
+ SourceFile(String text, {Object? url}) : this.decoded(text.runes, url: url);
+
+ /// Creates a new source file from [text].
+ ///
+ /// [url] may be either a [String], a [Uri], or `null`.
+ SourceFile.fromString(String text, {Object? url})
+ : this.decoded(text.codeUnits, url: url);
+
+ /// Creates a new source file from a list of decoded code units.
+ ///
+ /// [url] may be either a [String], a [Uri], or `null`.
+ ///
+ /// Currently, if [decodedChars] contains characters larger than `0xFFFF`,
+ /// they'll be treated as single characters rather than being split into
+ /// surrogate pairs. **This behavior is deprecated**. For
+ /// forwards-compatibility, callers should only pass in characters less than
+ /// or equal to `0xFFFF`.
+ SourceFile.decoded(Iterable<int> decodedChars, {Object? url})
+ : url = url is String ? Uri.parse(url) : url as Uri?,
+ _decodedChars = Uint32List.fromList(decodedChars.toList()) {
+ for (var i = 0; i < _decodedChars.length; i++) {
+ var c = _decodedChars[i];
+ if (c == _cr) {
+ // Return not followed by newline is treated as a newline
+ final j = i + 1;
+ if (j >= _decodedChars.length || _decodedChars[j] != _lf) c = _lf;
+ }
+ if (c == _lf) _lineStarts.add(i + 1);
+ }
+ }
+
+ /// Returns a span from [start] to [end] (exclusive).
+ ///
+ /// If [end] isn't passed, it defaults to the end of the file.
+ FileSpan span(int start, [int? end]) {
+ end ??= length;
+ return _FileSpan(this, start, end);
+ }
+
+ /// Returns a location at [offset].
+ FileLocation location(int offset) => FileLocation._(this, offset);
+
+ /// Gets the 0-based line corresponding to [offset].
+ int getLine(int offset) {
+ if (offset < 0) {
+ throw RangeError('Offset may not be negative, was $offset.');
+ } else if (offset > length) {
+ throw RangeError('Offset $offset must not be greater than the number '
+ 'of characters in the file, $length.');
+ }
+
+ if (offset < _lineStarts.first) return -1;
+ if (offset >= _lineStarts.last) return _lineStarts.length - 1;
+
+ if (_isNearCachedLine(offset)) return _cachedLine!;
+
+ _cachedLine = _binarySearch(offset) - 1;
+ return _cachedLine!;
+ }
+
+ /// Returns `true` if [offset] is near [_cachedLine].
+ ///
+ /// Checks on [_cachedLine] and the next line. If it's on the next line, it
+ /// updates [_cachedLine] to point to that.
+ bool _isNearCachedLine(int offset) {
+ if (_cachedLine == null) return false;
+ final cachedLine = _cachedLine!;
+
+ // See if it's before the cached line.
+ if (offset < _lineStarts[cachedLine]) return false;
+
+ // See if it's on the cached line.
+ if (cachedLine >= _lineStarts.length - 1 ||
+ offset < _lineStarts[cachedLine + 1]) {
+ return true;
+ }
+
+ // See if it's on the next line.
+ if (cachedLine >= _lineStarts.length - 2 ||
+ offset < _lineStarts[cachedLine + 2]) {
+ _cachedLine = cachedLine + 1;
+ return true;
+ }
+
+ return false;
+ }
+
+ /// Binary search through [_lineStarts] to find the line containing [offset].
+ ///
+ /// Returns the index of the line in [_lineStarts].
+ int _binarySearch(int offset) {
+ var min = 0;
+ var max = _lineStarts.length - 1;
+ while (min < max) {
+ final half = min + ((max - min) ~/ 2);
+ if (_lineStarts[half] > offset) {
+ max = half;
+ } else {
+ min = half + 1;
+ }
+ }
+
+ return max;
+ }
+
+ /// Gets the 0-based column corresponding to [offset].
+ ///
+ /// If [line] is passed, it's assumed to be the line containing [offset] and
+ /// is used to more efficiently compute the column.
+ int getColumn(int offset, {int? line}) {
+ if (offset < 0) {
+ throw RangeError('Offset may not be negative, was $offset.');
+ } else if (offset > length) {
+ throw RangeError('Offset $offset must be not be greater than the '
+ 'number of characters in the file, $length.');
+ }
+
+ if (line == null) {
+ line = getLine(offset);
+ } else if (line < 0) {
+ throw RangeError('Line may not be negative, was $line.');
+ } else if (line >= lines) {
+ throw RangeError('Line $line must be less than the number of '
+ 'lines in the file, $lines.');
+ }
+
+ final lineStart = _lineStarts[line];
+ if (lineStart > offset) {
+ throw RangeError('Line $line comes after offset $offset.');
+ }
+
+ return offset - lineStart;
+ }
+
+ /// Gets the offset for a [line] and [column].
+ ///
+ /// [column] defaults to 0.
+ int getOffset(int line, [int? column]) {
+ column ??= 0;
+
+ if (line < 0) {
+ throw RangeError('Line may not be negative, was $line.');
+ } else if (line >= lines) {
+ throw RangeError('Line $line must be less than the number of '
+ 'lines in the file, $lines.');
+ } else if (column < 0) {
+ throw RangeError('Column may not be negative, was $column.');
+ }
+
+ final result = _lineStarts[line] + column;
+ if (result > length ||
+ (line + 1 < lines && result >= _lineStarts[line + 1])) {
+ throw RangeError("Line $line doesn't have $column columns.");
+ }
+
+ return result;
+ }
+
+ /// Returns the text of the file from [start] to [end] (exclusive).
+ ///
+ /// If [end] isn't passed, it defaults to the end of the file.
+ String getText(int start, [int? end]) =>
+ String.fromCharCodes(_decodedChars.sublist(start, end));
+}
+
+/// A [SourceLocation] within a [SourceFile].
+///
+/// Unlike the base [SourceLocation], [FileLocation] lazily computes its line
+/// and column values based on its offset and the contents of [file].
+///
+/// A [FileLocation] can be created using [SourceFile.location].
+class FileLocation extends SourceLocationMixin implements SourceLocation {
+ /// The [file] that `this` belongs to.
+ final SourceFile file;
+
+ @override
+ final int offset;
+
+ @override
+ Uri? get sourceUrl => file.url;
+
+ @override
+ int get line => file.getLine(offset);
+
+ @override
+ int get column => file.getColumn(offset);
+
+ FileLocation._(this.file, this.offset) {
+ if (offset < 0) {
+ throw RangeError('Offset may not be negative, was $offset.');
+ } else if (offset > file.length) {
+ throw RangeError('Offset $offset must not be greater than the number '
+ 'of characters in the file, ${file.length}.');
+ }
+ }
+
+ @override
+ FileSpan pointSpan() => _FileSpan(file, offset, offset);
+}
+
+/// A [SourceSpan] within a [SourceFile].
+///
+/// Unlike the base [SourceSpan], [FileSpan] lazily computes its line and column
+/// values based on its offset and the contents of [file]. [SourceSpan.message]
+/// is also able to provide more context then [SourceSpan.message], and
+/// [SourceSpan.union] will return a [FileSpan] if possible.
+///
+/// A [FileSpan] can be created using [SourceFile.span].
+abstract class FileSpan implements SourceSpanWithContext {
+ /// The [file] that `this` belongs to.
+ SourceFile get file;
+
+ @override
+ FileLocation get start;
+
+ @override
+ FileLocation get end;
+
+ /// Returns a new span that covers both `this` and [other].
+ ///
+ /// Unlike [union], [other] may be disjoint from `this`. If it is, the text
+ /// between the two will be covered by the returned span.
+ FileSpan expand(FileSpan other);
+}
+
+/// The implementation of [FileSpan].
+///
+/// This is split into a separate class so that `is _FileSpan` checks can be run
+/// to make certain operations more efficient. If we used `is FileSpan`, that
+/// would break if external classes implemented the interface.
+class _FileSpan extends SourceSpanMixin implements FileSpan {
+ @override
+ final SourceFile file;
+
+ /// The offset of the beginning of the span.
+ ///
+ /// [start] is lazily generated from this to avoid allocating unnecessary
+ /// objects.
+ final int _start;
+
+ /// The offset of the end of the span.
+ ///
+ /// [end] is lazily generated from this to avoid allocating unnecessary
+ /// objects.
+ final int _end;
+
+ @override
+ Uri? get sourceUrl => file.url;
+
+ @override
+ int get length => _end - _start;
+
+ @override
+ FileLocation get start => FileLocation._(file, _start);
+
+ @override
+ FileLocation get end => FileLocation._(file, _end);
+
+ @override
+ String get text => file.getText(_start, _end);
+
+ @override
+ String get context {
+ final endLine = file.getLine(_end);
+ final endColumn = file.getColumn(_end);
+
+ int? endOffset;
+ if (endColumn == 0 && endLine != 0) {
+ // If [end] is at the very beginning of the line, the span covers the
+ // previous newline, so we only want to include the previous line in the
+ // context...
+
+ if (length == 0) {
+ // ...unless this is a point span, in which case we want to include the
+ // next line (or the empty string if this is the end of the file).
+ return endLine == file.lines - 1
+ ? ''
+ : file.getText(
+ file.getOffset(endLine), file.getOffset(endLine + 1));
+ }
+
+ endOffset = _end;
+ } else if (endLine == file.lines - 1) {
+ // If the span covers the last line of the file, the context should go all
+ // the way to the end of the file.
+ endOffset = file.length;
+ } else {
+ // Otherwise, the context should cover the full line on which [end]
+ // appears.
+ endOffset = file.getOffset(endLine + 1);
+ }
+
+ return file.getText(file.getOffset(file.getLine(_start)), endOffset);
+ }
+
+ _FileSpan(this.file, this._start, this._end) {
+ if (_end < _start) {
+ throw ArgumentError('End $_end must come after start $_start.');
+ } else if (_end > file.length) {
+ throw RangeError('End $_end must not be greater than the number '
+ 'of characters in the file, ${file.length}.');
+ } else if (_start < 0) {
+ throw RangeError('Start may not be negative, was $_start.');
+ }
+ }
+
+ @override
+ int compareTo(SourceSpan other) {
+ if (other is! _FileSpan) return super.compareTo(other);
+
+ final result = _start.compareTo(other._start);
+ return result == 0 ? _end.compareTo(other._end) : result;
+ }
+
+ @override
+ SourceSpan union(SourceSpan other) {
+ if (other is! FileSpan) return super.union(other);
+
+ final span = expand(other);
+
+ if (other is _FileSpan) {
+ if (_start > other._end || other._start > _end) {
+ throw ArgumentError('Spans $this and $other are disjoint.');
+ }
+ } else {
+ if (_start > other.end.offset || other.start.offset > _end) {
+ throw ArgumentError('Spans $this and $other are disjoint.');
+ }
+ }
+
+ return span;
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (other is! FileSpan) return super == other;
+ if (other is! _FileSpan) {
+ return super == other && sourceUrl == other.sourceUrl;
+ }
+
+ return _start == other._start &&
+ _end == other._end &&
+ sourceUrl == other.sourceUrl;
+ }
+
+ @override
+ int get hashCode => Object.hash(_start, _end, sourceUrl);
+
+ /// Returns a new span that covers both `this` and [other].
+ ///
+ /// Unlike [union], [other] may be disjoint from `this`. If it is, the text
+ /// between the two will be covered by the returned span.
+ @override
+ FileSpan expand(FileSpan other) {
+ if (sourceUrl != other.sourceUrl) {
+ throw ArgumentError('Source URLs "$sourceUrl" and '
+ " \"${other.sourceUrl}\" don't match.");
+ }
+
+ if (other is _FileSpan) {
+ final start = math.min(_start, other._start);
+ final end = math.max(_end, other._end);
+ return _FileSpan(file, start, end);
+ } else {
+ final start = math.min(_start, other.start.offset);
+ final end = math.max(_end, other.end.offset);
+ return _FileSpan(file, start, end);
+ }
+ }
+
+ /// See `SourceSpanExtension.subspan`.
+ FileSpan subspan(int start, [int? end]) {
+ RangeError.checkValidRange(start, end, length);
+ if (start == 0 && (end == null || end == length)) return this;
+ return file.span(_start + start, end == null ? _end : _start + end);
+ }
+}
+
+// TODO(#52): Move these to instance methods in the next breaking release.
+/// Extension methods on the [FileSpan] API.
+extension FileSpanExtension on FileSpan {
+ /// See `SourceSpanExtension.subspan`.
+ FileSpan subspan(int start, [int? end]) {
+ RangeError.checkValidRange(start, end, length);
+ if (start == 0 && (end == null || end == length)) return this;
+
+ final startOffset = this.start.offset;
+ return file.span(
+ startOffset + start, end == null ? this.end.offset : startOffset + end);
+ }
+}
diff --git a/pkgs/source_span/lib/src/highlighter.dart b/pkgs/source_span/lib/src/highlighter.dart
new file mode 100644
index 0000000..19e04d0
--- /dev/null
+++ b/pkgs/source_span/lib/src/highlighter.dart
@@ -0,0 +1,727 @@
+// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:math' as math;
+
+import 'package:collection/collection.dart';
+import 'package:path/path.dart' as p;
+import 'package:term_glyph/term_glyph.dart' as glyph;
+
+import 'charcode.dart';
+import 'colors.dart' as colors;
+import 'location.dart';
+import 'span.dart';
+import 'span_with_context.dart';
+import 'utils.dart';
+
+/// A class for writing a chunk of text with a particular span highlighted.
+class Highlighter {
+ /// The lines to display, including context around the highlighted spans.
+ final List<_Line> _lines;
+
+ /// The color to highlight the primary [_Highlight] within its context, or
+ /// `null` if it should not be colored.
+ final String? _primaryColor;
+
+ /// The color to highlight the secondary [_Highlight]s within their context,
+ /// or `null` if they should not be colored.
+ final String? _secondaryColor;
+
+ /// The number of characters before the bar in the sidebar.
+ final int _paddingBeforeSidebar;
+
+ /// The maximum number of multiline spans that cover any part of a single
+ /// line in [_lines].
+ final int _maxMultilineSpans;
+
+ /// Whether [_lines] includes lines from multiple different files.
+ final bool _multipleFiles;
+
+ /// The buffer to which to write the result.
+ final _buffer = StringBuffer();
+
+ /// The number of spaces to render for hard tabs that appear in `_span.text`.
+ ///
+ /// We don't want to render raw tabs, because they'll mess up our character
+ /// alignment.
+ static const _spacesPerTab = 4;
+
+ /// Creates a [Highlighter] that will return a string highlighting [span]
+ /// within the text of its file when [highlight] is called.
+ ///
+ /// [color] may either be a [String], a [bool], or `null`. If it's a string,
+ /// it indicates an [ANSI terminal color escape][] that should be used to
+ /// highlight [span]'s text (for example, `"\u001b[31m"` will color red). If
+ /// it's `true`, it indicates that the text should be highlighted using the
+ /// default color. If it's `false` or `null`, it indicates that no color
+ /// should be used.
+ ///
+ /// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+ Highlighter(SourceSpan span, {Object? color})
+ : this._(_collateLines([_Highlight(span, primary: true)]), () {
+ if (color == true) return colors.red;
+ if (color == false) return null;
+ return color as String?;
+ }(), null);
+
+ /// Creates a [Highlighter] that will return a string highlighting
+ /// [primarySpan] as well as all the spans in [secondarySpans] within the text
+ /// of their file when [highlight] is called.
+ ///
+ /// Each span has an associated label that will be written alongside it. For
+ /// [primarySpan] this message is [primaryLabel], and for [secondarySpans] the
+ /// labels are the map values.
+ ///
+ /// If [color] is `true`, this will use [ANSI terminal color escapes][] to
+ /// highlight the text. The [primarySpan] will be highlighted with
+ /// [primaryColor] (which defaults to red), and the [secondarySpans] will be
+ /// highlighted with [secondaryColor] (which defaults to blue). These
+ /// arguments are ignored if [color] is `false`.
+ ///
+ /// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+ Highlighter.multiple(SourceSpan primarySpan, String primaryLabel,
+ Map<SourceSpan, String> secondarySpans,
+ {bool color = false, String? primaryColor, String? secondaryColor})
+ : this._(
+ _collateLines([
+ _Highlight(primarySpan, label: primaryLabel, primary: true),
+ for (var entry in secondarySpans.entries)
+ _Highlight(entry.key, label: entry.value)
+ ]),
+ color ? (primaryColor ?? colors.red) : null,
+ color ? (secondaryColor ?? colors.blue) : null);
+
+ Highlighter._(this._lines, this._primaryColor, this._secondaryColor)
+ : _paddingBeforeSidebar = 1 +
+ math.max<int>(
+ // In a purely mathematical world, floor(log10(n)) would give the
+ // number of digits in n, but floating point errors render that
+ // unreliable in practice.
+ (_lines.last.number + 1).toString().length,
+ // If [_lines] aren't contiguous, we'll write "..." in place of a
+ // line number.
+ _contiguous(_lines) ? 0 : 3,
+ ),
+ _maxMultilineSpans = _lines
+ .map((line) => line.highlights
+ .where((highlight) => isMultiline(highlight.span))
+ .length)
+ .reduce(math.max),
+ _multipleFiles = !isAllTheSame(_lines.map((line) => line.url));
+
+ /// Returns whether [lines] contains any adjacent lines from the same source
+ /// file that aren't adjacent in the original file.
+ static bool _contiguous(List<_Line> lines) {
+ for (var i = 0; i < lines.length - 1; i++) {
+ final thisLine = lines[i];
+ final nextLine = lines[i + 1];
+ if (thisLine.number + 1 != nextLine.number &&
+ thisLine.url == nextLine.url) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /// Collect all the source lines from the contexts of all spans in
+ /// [highlights], and associates them with the highlights that cover them.
+ static List<_Line> _collateLines(List<_Highlight> highlights) {
+ // Assign spans without URLs opaque Objects as keys. Each such Object will
+ // be different, but they can then be used later on to determine which lines
+ // came from the same span even if they'd all otherwise have `null` URLs.
+ final highlightsByUrl = groupBy<_Highlight, Object>(
+ highlights, (highlight) => highlight.span.sourceUrl ?? Object());
+ for (var list in highlightsByUrl.values) {
+ list.sort((highlight1, highlight2) =>
+ highlight1.span.compareTo(highlight2.span));
+ }
+
+ return highlightsByUrl.entries.expand((entry) {
+ final url = entry.key;
+ final highlightsForFile = entry.value;
+
+ // First, create a list of all the lines in the current file that we have
+ // context for along with their line numbers.
+ final lines = <_Line>[];
+ for (var highlight in highlightsForFile) {
+ final context = highlight.span.context;
+ // If [highlight.span.context] contains lines prior to the one
+ // [highlight.span.text] appears on, write those first.
+ final lineStart = findLineStart(
+ context, highlight.span.text, highlight.span.start.column)!;
+
+ final linesBeforeSpan =
+ '\n'.allMatches(context.substring(0, lineStart)).length;
+
+ var lineNumber = highlight.span.start.line - linesBeforeSpan;
+ for (var line in context.split('\n')) {
+ // Only add a line if it hasn't already been added for a previous span
+ if (lines.isEmpty || lineNumber > lines.last.number) {
+ lines.add(_Line(line, lineNumber, url));
+ }
+ lineNumber++;
+ }
+ }
+
+ // Next, associate each line with each highlights that covers it.
+ final activeHighlights = <_Highlight>[];
+ var highlightIndex = 0;
+ for (var line in lines) {
+ activeHighlights
+ .removeWhere((highlight) => highlight.span.end.line < line.number);
+
+ final oldHighlightLength = activeHighlights.length;
+ for (var highlight in highlightsForFile.skip(highlightIndex)) {
+ if (highlight.span.start.line > line.number) break;
+ activeHighlights.add(highlight);
+ }
+ highlightIndex += activeHighlights.length - oldHighlightLength;
+
+ line.highlights.addAll(activeHighlights);
+ }
+
+ return lines;
+ }).toList();
+ }
+
+ /// Returns the highlighted span text.
+ ///
+ /// This method should only be called once.
+ String highlight() {
+ _writeFileStart(_lines.first.url);
+
+ // Each index of this list represents a column after the sidebar that could
+ // contain a line indicating an active highlight. If it's `null`, that
+ // column is empty; if it contains a highlight, it should be drawn for that
+ // column.
+ final highlightsByColumn =
+ List<_Highlight?>.filled(_maxMultilineSpans, null);
+
+ for (var i = 0; i < _lines.length; i++) {
+ final line = _lines[i];
+ if (i > 0) {
+ final lastLine = _lines[i - 1];
+ if (lastLine.url != line.url) {
+ _writeSidebar(end: glyph.upEnd);
+ _buffer.writeln();
+ _writeFileStart(line.url);
+ } else if (lastLine.number + 1 != line.number) {
+ _writeSidebar(text: '...');
+ _buffer.writeln();
+ }
+ }
+
+ // If a highlight covers the entire first line other than initial
+ // whitespace, don't bother pointing out exactly where it begins. Iterate
+ // in reverse so that longer highlights (which are sorted after shorter
+ // highlights) appear further out, leading to fewer crossed lines.
+ for (var highlight in line.highlights.reversed) {
+ if (isMultiline(highlight.span) &&
+ highlight.span.start.line == line.number &&
+ _isOnlyWhitespace(
+ line.text.substring(0, highlight.span.start.column))) {
+ replaceFirstNull(highlightsByColumn, highlight);
+ }
+ }
+
+ _writeSidebar(line: line.number);
+ _buffer.write(' ');
+ _writeMultilineHighlights(line, highlightsByColumn);
+ if (highlightsByColumn.isNotEmpty) _buffer.write(' ');
+
+ final primaryIdx =
+ line.highlights.indexWhere((highlight) => highlight.isPrimary);
+ final primary = primaryIdx == -1 ? null : line.highlights[primaryIdx];
+
+ if (primary != null) {
+ _writeHighlightedText(
+ line.text,
+ primary.span.start.line == line.number
+ ? primary.span.start.column
+ : 0,
+ primary.span.end.line == line.number
+ ? primary.span.end.column
+ : line.text.length,
+ color: _primaryColor);
+ } else {
+ _writeText(line.text);
+ }
+ _buffer.writeln();
+
+ // Always write the primary span's indicator first so that it's right next
+ // to the highlighted text.
+ if (primary != null) _writeIndicator(line, primary, highlightsByColumn);
+ for (var highlight in line.highlights) {
+ if (highlight.isPrimary) continue;
+ _writeIndicator(line, highlight, highlightsByColumn);
+ }
+ }
+
+ _writeSidebar(end: glyph.upEnd);
+ return _buffer.toString();
+ }
+
+ /// Writes the beginning of the file highlight for the file with the given
+ /// [url] (or opaque object if it comes from a span with a null URL).
+ void _writeFileStart(Object url) {
+ if (!_multipleFiles || url is! Uri) {
+ _writeSidebar(end: glyph.downEnd);
+ } else {
+ _writeSidebar(end: glyph.topLeftCorner);
+ _colorize(() => _buffer.write('${glyph.horizontalLine * 2}>'),
+ color: colors.blue);
+ _buffer.write(' ${p.prettyUri(url)}');
+ }
+ _buffer.writeln();
+ }
+
+ /// Writes the post-sidebar highlight bars for [line] according to
+ /// [highlightsByColumn].
+ ///
+ /// If [current] is passed, it's the highlight for which an indicator is being
+ /// written. If it appears in [highlightsByColumn], a horizontal line is
+ /// written from its column to the rightmost column.
+ void _writeMultilineHighlights(
+ _Line line, List<_Highlight?> highlightsByColumn,
+ {_Highlight? current}) {
+ // Whether we've written a sidebar indicator for opening a new span on this
+ // line, and which color should be used for that indicator's rightward line.
+ var openedOnThisLine = false;
+ String? openedOnThisLineColor;
+
+ final currentColor = current == null
+ ? null
+ : current.isPrimary
+ ? _primaryColor
+ : _secondaryColor;
+ var foundCurrent = false;
+ for (var highlight in highlightsByColumn) {
+ final startLine = highlight?.span.start.line;
+ final endLine = highlight?.span.end.line;
+ if (current != null && highlight == current) {
+ foundCurrent = true;
+ assert(startLine == line.number || endLine == line.number);
+ _colorize(() {
+ _buffer.write(startLine == line.number
+ ? glyph.topLeftCorner
+ : glyph.bottomLeftCorner);
+ }, color: currentColor);
+ } else if (foundCurrent) {
+ _colorize(() {
+ _buffer.write(highlight == null ? glyph.horizontalLine : glyph.cross);
+ }, color: currentColor);
+ } else if (highlight == null) {
+ if (openedOnThisLine) {
+ _colorize(() => _buffer.write(glyph.horizontalLine),
+ color: openedOnThisLineColor);
+ } else {
+ _buffer.write(' ');
+ }
+ } else {
+ _colorize(() {
+ final vertical = openedOnThisLine ? glyph.cross : glyph.verticalLine;
+ if (current != null) {
+ _buffer.write(vertical);
+ } else if (startLine == line.number) {
+ _colorize(() {
+ _buffer
+ .write(glyph.glyphOrAscii(openedOnThisLine ? '┬' : '┌', '/'));
+ }, color: openedOnThisLineColor);
+ openedOnThisLine = true;
+ openedOnThisLineColor ??=
+ highlight.isPrimary ? _primaryColor : _secondaryColor;
+ } else if (endLine == line.number &&
+ highlight.span.end.column == line.text.length) {
+ _buffer.write(highlight.label == null
+ ? glyph.glyphOrAscii('â””', r'\')
+ : vertical);
+ } else {
+ _colorize(() {
+ _buffer.write(vertical);
+ }, color: openedOnThisLineColor);
+ }
+ }, color: highlight.isPrimary ? _primaryColor : _secondaryColor);
+ }
+ }
+ }
+
+ // Writes [text], with text between [startColumn] and [endColumn] colorized in
+ // the same way as [_colorize].
+ void _writeHighlightedText(String text, int startColumn, int endColumn,
+ {required String? color}) {
+ _writeText(text.substring(0, startColumn));
+ _colorize(() => _writeText(text.substring(startColumn, endColumn)),
+ color: color);
+ _writeText(text.substring(endColumn, text.length));
+ }
+
+ /// Writes an indicator for where [highlight] starts, ends, or both below
+ /// [line].
+ ///
+ /// This may either add or remove [highlight] from [highlightsByColumn].
+ void _writeIndicator(
+ _Line line, _Highlight highlight, List<_Highlight?> highlightsByColumn) {
+ final color = highlight.isPrimary ? _primaryColor : _secondaryColor;
+ if (!isMultiline(highlight.span)) {
+ _writeSidebar();
+ _buffer.write(' ');
+ _writeMultilineHighlights(line, highlightsByColumn, current: highlight);
+ if (highlightsByColumn.isNotEmpty) _buffer.write(' ');
+
+ final underlineLength = _colorize(() {
+ final start = _buffer.length;
+ _writeUnderline(line, highlight.span,
+ highlight.isPrimary ? '^' : glyph.horizontalLineBold);
+ return _buffer.length - start;
+ }, color: color);
+ _writeLabel(highlight, highlightsByColumn, underlineLength);
+ } else if (highlight.span.start.line == line.number) {
+ if (highlightsByColumn.contains(highlight)) return;
+ replaceFirstNull(highlightsByColumn, highlight);
+
+ _writeSidebar();
+ _buffer.write(' ');
+ _writeMultilineHighlights(line, highlightsByColumn, current: highlight);
+ _colorize(() => _writeArrow(line, highlight.span.start.column),
+ color: color);
+ _buffer.writeln();
+ } else if (highlight.span.end.line == line.number) {
+ final coversWholeLine = highlight.span.end.column == line.text.length;
+ if (coversWholeLine && highlight.label == null) {
+ replaceWithNull(highlightsByColumn, highlight);
+ return;
+ }
+
+ _writeSidebar();
+ _buffer.write(' ');
+ _writeMultilineHighlights(line, highlightsByColumn, current: highlight);
+
+ final underlineLength = _colorize(() {
+ final start = _buffer.length;
+ if (coversWholeLine) {
+ _buffer.write(glyph.horizontalLine * 3);
+ } else {
+ _writeArrow(line, math.max(highlight.span.end.column - 1, 0),
+ beginning: false);
+ }
+ return _buffer.length - start;
+ }, color: color);
+ _writeLabel(highlight, highlightsByColumn, underlineLength);
+ replaceWithNull(highlightsByColumn, highlight);
+ }
+ }
+
+ /// Underlines the portion of [line] covered by [span] with repeated instances
+ /// of [character].
+ void _writeUnderline(_Line line, SourceSpan span, String character) {
+ assert(!isMultiline(span));
+ assert(line.text.contains(span.text),
+ '"${line.text}" should contain "${span.text}"');
+
+ var startColumn = span.start.column;
+ var endColumn = span.end.column;
+
+ // Adjust the start and end columns to account for any tabs that were
+ // converted to spaces.
+ final tabsBefore = _countTabs(line.text.substring(0, startColumn));
+ final tabsInside = _countTabs(line.text.substring(startColumn, endColumn));
+ startColumn += tabsBefore * (_spacesPerTab - 1);
+ endColumn += (tabsBefore + tabsInside) * (_spacesPerTab - 1);
+
+ _buffer
+ ..write(' ' * startColumn)
+ ..write(character * math.max(endColumn - startColumn, 1));
+ }
+
+ /// Write an arrow pointing to column [column] in [line].
+ ///
+ /// If the arrow points to a tab character, this will point to the beginning
+ /// of the tab if [beginning] is `true` and the end if it's `false`.
+ void _writeArrow(_Line line, int column, {bool beginning = true}) {
+ final tabs =
+ _countTabs(line.text.substring(0, column + (beginning ? 0 : 1)));
+ _buffer
+ ..write(glyph.horizontalLine * (1 + column + tabs * (_spacesPerTab - 1)))
+ ..write('^');
+ }
+
+ /// Writes [highlight]'s label.
+ ///
+ /// The `_buffer` is assumed to be written to the point where the first line
+ /// of `highlight.label` can be written after a space, but this takes care of
+ /// writing indentation and highlight columns for later lines.
+ ///
+ /// The [highlightsByColumn] are used to write ongoing highlight lines if the
+ /// label is more than one line long.
+ ///
+ /// The [underlineLength] is the length of the line written between the
+ /// highlights and the beginning of the first label.
+ void _writeLabel(_Highlight highlight, List<_Highlight?> highlightsByColumn,
+ int underlineLength) {
+ final label = highlight.label;
+ if (label == null) {
+ _buffer.writeln();
+ return;
+ }
+
+ final lines = label.split('\n');
+ final color = highlight.isPrimary ? _primaryColor : _secondaryColor;
+ _colorize(() => _buffer.write(' ${lines.first}'), color: color);
+ _buffer.writeln();
+
+ for (var text in lines.skip(1)) {
+ _writeSidebar();
+ _buffer.write(' ');
+ for (var columnHighlight in highlightsByColumn) {
+ if (columnHighlight == null || columnHighlight == highlight) {
+ _buffer.write(' ');
+ } else {
+ _buffer.write(glyph.verticalLine);
+ }
+ }
+
+ _buffer.write(' ' * underlineLength);
+ _colorize(() => _buffer.write(' $text'), color: color);
+ _buffer.writeln();
+ }
+ }
+
+ /// Writes a snippet from the source text, converting hard tab characters into
+ /// plain indentation.
+ void _writeText(String text) {
+ for (var char in text.codeUnits) {
+ if (char == $tab) {
+ _buffer.write(' ' * _spacesPerTab);
+ } else {
+ _buffer.writeCharCode(char);
+ }
+ }
+ }
+
+ // Writes a sidebar to [buffer] that includes [line] as the line number if
+ // given and writes [end] at the end (defaults to [glyphs.verticalLine]).
+ //
+ // If [text] is given, it's used in place of the line number. It can't be
+ // passed at the same time as [line].
+ void _writeSidebar({int? line, String? text, String? end}) {
+ assert(line == null || text == null);
+
+ // Add 1 to line to convert from computer-friendly 0-indexed line numbers to
+ // human-friendly 1-indexed line numbers.
+ if (line != null) text = (line + 1).toString();
+ _colorize(() {
+ _buffer
+ ..write((text ?? '').padRight(_paddingBeforeSidebar))
+ ..write(end ?? glyph.verticalLine);
+ }, color: colors.blue);
+ }
+
+ /// Returns the number of hard tabs in [text].
+ int _countTabs(String text) {
+ var count = 0;
+ for (var char in text.codeUnits) {
+ if (char == $tab) count++;
+ }
+ return count;
+ }
+
+ /// Returns whether [text] contains only space or tab characters.
+ bool _isOnlyWhitespace(String text) {
+ for (var char in text.codeUnits) {
+ if (char != $space && char != $tab) return false;
+ }
+ return true;
+ }
+
+ /// Colors all text written to [_buffer] during [callback], if colorization is
+ /// enabled and [color] is not `null`.
+ T _colorize<T>(T Function() callback, {required String? color}) {
+ if (_primaryColor != null && color != null) _buffer.write(color);
+ final result = callback();
+ if (_primaryColor != null && color != null) _buffer.write(colors.none);
+ return result;
+ }
+}
+
+/// Information about how to highlight a single section of a source file.
+class _Highlight {
+ /// The section of the source file to highlight.
+ ///
+ /// This is normalized to make it easier for [Highlighter] to work with.
+ final SourceSpanWithContext span;
+
+ /// Whether this is the primary span in the highlight.
+ ///
+ /// The primary span is highlighted with a different character and colored
+ /// differently than non-primary spans.
+ final bool isPrimary;
+
+ /// The label to include inline when highlighting [span].
+ ///
+ /// This helps distinguish clarify what each highlight means when multiple are
+ /// used in the same message.
+ final String? label;
+
+ _Highlight(SourceSpan span, {String? label, bool primary = false})
+ : span = (() {
+ var newSpan = _normalizeContext(span);
+ newSpan = _normalizeNewlines(newSpan);
+ newSpan = _normalizeTrailingNewline(newSpan);
+ return _normalizeEndOfLine(newSpan);
+ })(),
+ isPrimary = primary,
+ label = label?.replaceAll('\r\n', '\n');
+
+ /// Normalizes [span] to ensure that it's a [SourceSpanWithContext] whose
+ /// context actually contains its text at the expected column.
+ ///
+ /// If it's not already a [SourceSpanWithContext], adjust the start and end
+ /// locations' line and column fields so that the highlighter can assume they
+ /// match up with the context.
+ static SourceSpanWithContext _normalizeContext(SourceSpan span) =>
+ span is SourceSpanWithContext &&
+ findLineStart(span.context, span.text, span.start.column) != null
+ ? span
+ : SourceSpanWithContext(
+ SourceLocation(span.start.offset,
+ sourceUrl: span.sourceUrl, line: 0, column: 0),
+ SourceLocation(span.end.offset,
+ sourceUrl: span.sourceUrl,
+ line: countCodeUnits(span.text, $lf),
+ column: _lastLineLength(span.text)),
+ span.text,
+ span.text);
+
+ /// Normalizes [span] to replace Windows-style newlines with Unix-style
+ /// newlines.
+ static SourceSpanWithContext _normalizeNewlines(SourceSpanWithContext span) {
+ final text = span.text;
+ if (!text.contains('\r\n')) return span;
+
+ var endOffset = span.end.offset;
+ for (var i = 0; i < text.length - 1; i++) {
+ if (text.codeUnitAt(i) == $cr && text.codeUnitAt(i + 1) == $lf) {
+ endOffset--;
+ }
+ }
+
+ return SourceSpanWithContext(
+ span.start,
+ SourceLocation(endOffset,
+ sourceUrl: span.sourceUrl,
+ line: span.end.line,
+ column: span.end.column),
+ text.replaceAll('\r\n', '\n'),
+ span.context.replaceAll('\r\n', '\n'));
+ }
+
+ /// Normalizes [span] to remove a trailing newline from `span.context`.
+ ///
+ /// If necessary, also adjust `span.end` so that it doesn't point past where
+ /// the trailing newline used to be.
+ static SourceSpanWithContext _normalizeTrailingNewline(
+ SourceSpanWithContext span) {
+ if (!span.context.endsWith('\n')) return span;
+
+ // If there's a full blank line on the end of [span.context], it's probably
+ // significant, so we shouldn't trim it.
+ if (span.text.endsWith('\n\n')) return span;
+
+ final context = span.context.substring(0, span.context.length - 1);
+ var text = span.text;
+ var start = span.start;
+ var end = span.end;
+ if (span.text.endsWith('\n') && _isTextAtEndOfContext(span)) {
+ text = span.text.substring(0, span.text.length - 1);
+ if (text.isEmpty) {
+ end = start;
+ } else {
+ end = SourceLocation(span.end.offset - 1,
+ sourceUrl: span.sourceUrl,
+ line: span.end.line - 1,
+ column: _lastLineLength(context));
+ start = span.start.offset == span.end.offset ? end : span.start;
+ }
+ }
+ return SourceSpanWithContext(start, end, text, context);
+ }
+
+ /// Normalizes [span] so that the end location is at the end of a line rather
+ /// than at the beginning of the next line.
+ static SourceSpanWithContext _normalizeEndOfLine(SourceSpanWithContext span) {
+ if (span.end.column != 0) return span;
+ if (span.end.line == span.start.line) return span;
+
+ final text = span.text.substring(0, span.text.length - 1);
+
+ return SourceSpanWithContext(
+ span.start,
+ SourceLocation(span.end.offset - 1,
+ sourceUrl: span.sourceUrl,
+ line: span.end.line - 1,
+ column: text.length - text.lastIndexOf('\n') - 1),
+ text,
+ // If the context also ends with a newline, it's possible that we don't
+ // have the full context for that line, so we shouldn't print it at all.
+ span.context.endsWith('\n')
+ ? span.context.substring(0, span.context.length - 1)
+ : span.context);
+ }
+
+ /// Returns the length of the last line in [text], whether or not it ends in a
+ /// newline.
+ static int _lastLineLength(String text) {
+ if (text.isEmpty) {
+ return 0;
+ } else if (text.codeUnitAt(text.length - 1) == $lf) {
+ return text.length == 1
+ ? 0
+ : text.length - text.lastIndexOf('\n', text.length - 2) - 1;
+ } else {
+ return text.length - text.lastIndexOf('\n') - 1;
+ }
+ }
+
+ /// Returns whether [span]'s text runs all the way to the end of its context.
+ static bool _isTextAtEndOfContext(SourceSpanWithContext span) =>
+ findLineStart(span.context, span.text, span.start.column)! +
+ span.start.column +
+ span.length ==
+ span.context.length;
+
+ @override
+ String toString() {
+ final buffer = StringBuffer();
+ if (isPrimary) buffer.write('primary ');
+ buffer.write('${span.start.line}:${span.start.column}-'
+ '${span.end.line}:${span.end.column}');
+ if (label != null) buffer.write(' ($label)');
+ return buffer.toString();
+ }
+}
+
+/// A single line of the source file being highlighted.
+class _Line {
+ /// The text of the line, not including the trailing newline.
+ final String text;
+
+ /// The 0-based line number in the source file.
+ final int number;
+
+ /// The URL of the source file in which this line appears.
+ ///
+ /// For lines created from spans without an explicit URL, this is an opaque
+ /// object that differs between lines that come from different spans.
+ final Object url;
+
+ /// All highlights that cover any portion of this line, in source span order.
+ ///
+ /// This is populated after the initial line is created.
+ final highlights = <_Highlight>[];
+
+ _Line(this.text, this.number, this.url);
+
+ @override
+ String toString() => '$number: "$text" (${highlights.join(', ')})';
+}
diff --git a/pkgs/source_span/lib/src/location.dart b/pkgs/source_span/lib/src/location.dart
new file mode 100644
index 0000000..8f22d7b
--- /dev/null
+++ b/pkgs/source_span/lib/src/location.dart
@@ -0,0 +1,102 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'span.dart';
+
+// TODO(nweiz): Use SourceLocationMixin once we decide to cut a release with
+// breaking changes. See SourceLocationMixin for details.
+
+/// A class that describes a single location within a source file.
+///
+/// This class should not be extended. Instead, [SourceLocationBase] should be
+/// extended instead.
+class SourceLocation implements Comparable<SourceLocation> {
+ /// URL of the source containing this location.
+ ///
+ /// This may be null, indicating that the source URL is unknown or
+ /// unavailable.
+ final Uri? sourceUrl;
+
+ /// The 0-based offset of this location in the source.
+ final int offset;
+
+ /// The 0-based line of this location in the source.
+ final int line;
+
+ /// The 0-based column of this location in the source
+ final int column;
+
+ /// Returns a representation of this location in the `source:line:column`
+ /// format used by text editors.
+ ///
+ /// This prints 1-based lines and columns.
+ String get toolString {
+ final source = sourceUrl ?? 'unknown source';
+ return '$source:${line + 1}:${column + 1}';
+ }
+
+ /// Creates a new location indicating [offset] within [sourceUrl].
+ ///
+ /// [line] and [column] default to assuming the source is a single line. This
+ /// means that [line] defaults to 0 and [column] defaults to [offset].
+ ///
+ /// [sourceUrl] may be either a [String], a [Uri], or `null`.
+ SourceLocation(this.offset, {Object? sourceUrl, int? line, int? column})
+ : sourceUrl =
+ sourceUrl is String ? Uri.parse(sourceUrl) : sourceUrl as Uri?,
+ line = line ?? 0,
+ column = column ?? offset {
+ if (offset < 0) {
+ throw RangeError('Offset may not be negative, was $offset.');
+ } else if (line != null && line < 0) {
+ throw RangeError('Line may not be negative, was $line.');
+ } else if (column != null && column < 0) {
+ throw RangeError('Column may not be negative, was $column.');
+ }
+ }
+
+ /// Returns the distance in characters between `this` and [other].
+ ///
+ /// This always returns a non-negative value.
+ int distance(SourceLocation other) {
+ if (sourceUrl != other.sourceUrl) {
+ throw ArgumentError('Source URLs "$sourceUrl" and '
+ "\"${other.sourceUrl}\" don't match.");
+ }
+ return (offset - other.offset).abs();
+ }
+
+ /// Returns a span that covers only a single point: this location.
+ SourceSpan pointSpan() => SourceSpan(this, this, '');
+
+ /// Compares two locations.
+ ///
+ /// [other] must have the same source URL as `this`.
+ @override
+ int compareTo(SourceLocation other) {
+ if (sourceUrl != other.sourceUrl) {
+ throw ArgumentError('Source URLs "$sourceUrl" and '
+ "\"${other.sourceUrl}\" don't match.");
+ }
+ return offset - other.offset;
+ }
+
+ @override
+ bool operator ==(Object other) =>
+ other is SourceLocation &&
+ sourceUrl == other.sourceUrl &&
+ offset == other.offset;
+
+ @override
+ int get hashCode => (sourceUrl?.hashCode ?? 0) + offset;
+
+ @override
+ String toString() => '<$runtimeType: $offset $toolString>';
+}
+
+/// A base class for source locations with [offset], [line], and [column] known
+/// at construction time.
+class SourceLocationBase extends SourceLocation {
+ SourceLocationBase(super.offset, {super.sourceUrl, super.line, super.column});
+}
diff --git a/pkgs/source_span/lib/src/location_mixin.dart b/pkgs/source_span/lib/src/location_mixin.dart
new file mode 100644
index 0000000..a44f5e2
--- /dev/null
+++ b/pkgs/source_span/lib/src/location_mixin.dart
@@ -0,0 +1,55 @@
+// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'location.dart';
+import 'span.dart';
+
+// Note: this class duplicates a lot of functionality of [SourceLocation]. This
+// is because in order for SourceLocation to use SourceLocationMixin,
+// SourceLocationMixin couldn't implement SourceLocation. In SourceSpan we
+// handle this by making the class itself non-extensible, but that would be a
+// breaking change for SourceLocation. So until we want to endure the pain of
+// cutting a release with breaking changes, we duplicate the code here.
+
+/// A mixin for easily implementing [SourceLocation].
+abstract class SourceLocationMixin implements SourceLocation {
+ @override
+ String get toolString {
+ final source = sourceUrl ?? 'unknown source';
+ return '$source:${line + 1}:${column + 1}';
+ }
+
+ @override
+ int distance(SourceLocation other) {
+ if (sourceUrl != other.sourceUrl) {
+ throw ArgumentError('Source URLs "$sourceUrl" and '
+ "\"${other.sourceUrl}\" don't match.");
+ }
+ return (offset - other.offset).abs();
+ }
+
+ @override
+ SourceSpan pointSpan() => SourceSpan(this, this, '');
+
+ @override
+ int compareTo(SourceLocation other) {
+ if (sourceUrl != other.sourceUrl) {
+ throw ArgumentError('Source URLs "$sourceUrl" and '
+ "\"${other.sourceUrl}\" don't match.");
+ }
+ return offset - other.offset;
+ }
+
+ @override
+ bool operator ==(Object other) =>
+ other is SourceLocation &&
+ sourceUrl == other.sourceUrl &&
+ offset == other.offset;
+
+ @override
+ int get hashCode => (sourceUrl?.hashCode ?? 0) + offset;
+
+ @override
+ String toString() => '<$runtimeType: $offset $toolString>';
+}
diff --git a/pkgs/source_span/lib/src/span.dart b/pkgs/source_span/lib/src/span.dart
new file mode 100644
index 0000000..941dedc
--- /dev/null
+++ b/pkgs/source_span/lib/src/span.dart
@@ -0,0 +1,193 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as p;
+import 'package:term_glyph/term_glyph.dart' as glyph;
+
+import 'file.dart';
+import 'highlighter.dart';
+import 'location.dart';
+import 'span_mixin.dart';
+import 'span_with_context.dart';
+import 'utils.dart';
+
+/// A class that describes a segment of source text.
+abstract class SourceSpan implements Comparable<SourceSpan> {
+ /// The start location of this span.
+ SourceLocation get start;
+
+ /// The end location of this span, exclusive.
+ SourceLocation get end;
+
+ /// The source text for this span.
+ String get text;
+
+ /// The URL of the source (typically a file) of this span.
+ ///
+ /// This may be null, indicating that the source URL is unknown or
+ /// unavailable.
+ Uri? get sourceUrl;
+
+ /// The length of this span, in characters.
+ int get length;
+
+ /// Creates a new span from [start] to [end] (exclusive) containing [text].
+ ///
+ /// [start] and [end] must have the same source URL and [start] must come
+ /// before [end]. [text] must have a number of characters equal to the
+ /// distance between [start] and [end].
+ factory SourceSpan(SourceLocation start, SourceLocation end, String text) =>
+ SourceSpanBase(start, end, text);
+
+ /// Creates a new span that's the union of `this` and [other].
+ ///
+ /// The two spans must have the same source URL and may not be disjoint.
+ /// [text] is computed by combining `this.text` and `other.text`.
+ SourceSpan union(SourceSpan other);
+
+ /// Compares two spans.
+ ///
+ /// [other] must have the same source URL as `this`. This orders spans by
+ /// [start] then [length].
+ @override
+ int compareTo(SourceSpan other);
+
+ /// Formats [message] in a human-friendly way associated with this span.
+ ///
+ /// [color] may either be a [String], a [bool], or `null`. If it's a string,
+ /// it indicates an [ANSI terminal color escape][] that should
+ /// be used to highlight the span's text (for example, `"\u001b[31m"` will
+ /// color red). If it's `true`, it indicates that the text should be
+ /// highlighted using the default color. If it's `false` or `null`, it
+ /// indicates that the text shouldn't be highlighted.
+ ///
+ /// This uses the full range of Unicode characters to highlight the source
+ /// span if [glyph.ascii] is `false` (the default), but only uses ASCII
+ /// characters if it's `true`.
+ ///
+ /// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+ String message(String message, {Object? color});
+
+ /// Prints the text associated with this span in a user-friendly way.
+ ///
+ /// This is identical to [message], except that it doesn't print the file
+ /// name, line number, column number, or message. If [length] is 0 and this
+ /// isn't a [SourceSpanWithContext], returns an empty string.
+ ///
+ /// [color] may either be a [String], a [bool], or `null`. If it's a string,
+ /// it indicates an [ANSI terminal color escape][] that should
+ /// be used to highlight the span's text (for example, `"\u001b[31m"` will
+ /// color red). If it's `true`, it indicates that the text should be
+ /// highlighted using the default color. If it's `false` or `null`, it
+ /// indicates that the text shouldn't be highlighted.
+ ///
+ /// This uses the full range of Unicode characters to highlight the source
+ /// span if [glyph.ascii] is `false` (the default), but only uses ASCII
+ /// characters if it's `true`.
+ ///
+ /// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+ String highlight({Object? color});
+}
+
+/// A base class for source spans with [start], [end], and [text] known at
+/// construction time.
+class SourceSpanBase extends SourceSpanMixin {
+ @override
+ final SourceLocation start;
+ @override
+ final SourceLocation end;
+ @override
+ final String text;
+
+ SourceSpanBase(this.start, this.end, this.text) {
+ if (end.sourceUrl != start.sourceUrl) {
+ throw ArgumentError('Source URLs "${start.sourceUrl}" and '
+ " \"${end.sourceUrl}\" don't match.");
+ } else if (end.offset < start.offset) {
+ throw ArgumentError('End $end must come after start $start.');
+ } else if (text.length != start.distance(end)) {
+ throw ArgumentError('Text "$text" must be ${start.distance(end)} '
+ 'characters long.');
+ }
+ }
+}
+
+// TODO(#52): Move these to instance methods in the next breaking release.
+/// Extension methods on the base [SourceSpan] API.
+extension SourceSpanExtension on SourceSpan {
+ /// Like [SourceSpan.message], but also highlights [secondarySpans] to provide
+ /// the user with additional context.
+ ///
+ /// Each span takes a label ([label] for this span, and the values of the
+ /// [secondarySpans] map for the secondary spans) that's used to indicate to
+ /// the user what that particular span represents.
+ ///
+ /// If [color] is `true`, [ANSI terminal color escapes][] are used to color
+ /// the resulting string. By default this span is colored red and the
+ /// secondary spans are colored blue, but that can be customized by passing
+ /// ANSI escape strings to [primaryColor] or [secondaryColor].
+ ///
+ /// [ANSI terminal color escapes]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+ ///
+ /// Each span in [secondarySpans] must refer to the same document as this
+ /// span. Throws an [ArgumentError] if any secondary span has a different
+ /// source URL than this span.
+ ///
+ /// Note that while this will work with plain [SourceSpan]s, it will produce
+ /// much more useful output with [SourceSpanWithContext]s (including
+ /// [FileSpan]s).
+ String messageMultiple(
+ String message, String label, Map<SourceSpan, String> secondarySpans,
+ {bool color = false, String? primaryColor, String? secondaryColor}) {
+ final buffer = StringBuffer()
+ ..write('line ${start.line + 1}, column ${start.column + 1}');
+ if (sourceUrl != null) buffer.write(' of ${p.prettyUri(sourceUrl)}');
+ buffer
+ ..writeln(': $message')
+ ..write(highlightMultiple(label, secondarySpans,
+ color: color,
+ primaryColor: primaryColor,
+ secondaryColor: secondaryColor));
+ return buffer.toString();
+ }
+
+ /// Like [SourceSpan.highlight], but also highlights [secondarySpans] to
+ /// provide the user with additional context.
+ ///
+ /// Each span takes a label ([label] for this span, and the values of the
+ /// [secondarySpans] map for the secondary spans) that's used to indicate to
+ /// the user what that particular span represents.
+ ///
+ /// If [color] is `true`, [ANSI terminal color escapes][] are used to color
+ /// the resulting string. By default this span is colored red and the
+ /// secondary spans are colored blue, but that can be customized by passing
+ /// ANSI escape strings to [primaryColor] or [secondaryColor].
+ ///
+ /// [ANSI terminal color escapes]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+ ///
+ /// Each span in [secondarySpans] must refer to the same document as this
+ /// span. Throws an [ArgumentError] if any secondary span has a different
+ /// source URL than this span.
+ ///
+ /// Note that while this will work with plain [SourceSpan]s, it will produce
+ /// much more useful output with [SourceSpanWithContext]s (including
+ /// [FileSpan]s).
+ String highlightMultiple(String label, Map<SourceSpan, String> secondarySpans,
+ {bool color = false, String? primaryColor, String? secondaryColor}) =>
+ Highlighter.multiple(this, label, secondarySpans,
+ color: color,
+ primaryColor: primaryColor,
+ secondaryColor: secondaryColor)
+ .highlight();
+
+ /// Returns a span from [start] code units (inclusive) to [end] code units
+ /// (exclusive) after the beginning of this span.
+ SourceSpan subspan(int start, [int? end]) {
+ RangeError.checkValidRange(start, end, length);
+ if (start == 0 && (end == null || end == length)) return this;
+
+ final locations = subspanLocations(this, start, end);
+ return SourceSpan(locations[0], locations[1], text.substring(start, end));
+ }
+}
diff --git a/pkgs/source_span/lib/src/span_exception.dart b/pkgs/source_span/lib/src/span_exception.dart
new file mode 100644
index 0000000..90ad690
--- /dev/null
+++ b/pkgs/source_span/lib/src/span_exception.dart
@@ -0,0 +1,114 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'span.dart';
+
+/// A class for exceptions that have source span information attached.
+class SourceSpanException implements Exception {
+ // This is a getter so that subclasses can override it.
+ /// A message describing the exception.
+ String get message => _message;
+ final String _message;
+
+ // This is a getter so that subclasses can override it.
+ /// The span associated with this exception.
+ ///
+ /// This may be `null` if the source location can't be determined.
+ SourceSpan? get span => _span;
+ final SourceSpan? _span;
+
+ SourceSpanException(this._message, this._span);
+
+ /// Returns a string representation of `this`.
+ ///
+ /// [color] may either be a [String], a [bool], or `null`. If it's a string,
+ /// it indicates an ANSI terminal color escape that should be used to
+ /// highlight the span's text. If it's `true`, it indicates that the text
+ /// should be highlighted using the default color. If it's `false` or `null`,
+ /// it indicates that the text shouldn't be highlighted.
+ @override
+ String toString({Object? color}) {
+ if (span == null) return message;
+ return 'Error on ${span!.message(message, color: color)}';
+ }
+}
+
+/// A [SourceSpanException] that's also a [FormatException].
+class SourceSpanFormatException extends SourceSpanException
+ implements FormatException {
+ @override
+ final dynamic source;
+
+ @override
+ int? get offset => span?.start.offset;
+
+ SourceSpanFormatException(super.message, super.span, [this.source]);
+}
+
+/// A [SourceSpanException] that also highlights some secondary spans to provide
+/// the user with extra context.
+///
+/// Each span has a label ([primaryLabel] for the primary, and the values of the
+/// [secondarySpans] map for the secondary spans) that's used to indicate to the
+/// user what that particular span represents.
+class MultiSourceSpanException extends SourceSpanException {
+ /// A label to attach to [span] that provides additional information and helps
+ /// distinguish it from [secondarySpans].
+ final String primaryLabel;
+
+ /// A map whose keys are secondary spans that should be highlighted.
+ ///
+ /// Each span's value is a label to attach to that span that provides
+ /// additional information and helps distinguish it from [secondarySpans].
+ final Map<SourceSpan, String> secondarySpans;
+
+ MultiSourceSpanException(super.message, super.span, this.primaryLabel,
+ Map<SourceSpan, String> secondarySpans)
+ : secondarySpans = Map.unmodifiable(secondarySpans);
+
+ /// Returns a string representation of `this`.
+ ///
+ /// [color] may either be a [String], a [bool], or `null`. If it's a string,
+ /// it indicates an ANSI terminal color escape that should be used to
+ /// highlight the primary span's text. If it's `true`, it indicates that the
+ /// text should be highlighted using the default color. If it's `false` or
+ /// `null`, it indicates that the text shouldn't be highlighted.
+ ///
+ /// If [color] is `true` or a string, [secondaryColor] is used to highlight
+ /// [secondarySpans].
+ @override
+ String toString({Object? color, String? secondaryColor}) {
+ if (span == null) return message;
+
+ var useColor = false;
+ String? primaryColor;
+ if (color is String) {
+ useColor = true;
+ primaryColor = color;
+ } else if (color == true) {
+ useColor = true;
+ }
+
+ final formatted = span!.messageMultiple(
+ message, primaryLabel, secondarySpans,
+ color: useColor,
+ primaryColor: primaryColor,
+ secondaryColor: secondaryColor);
+ return 'Error on $formatted';
+ }
+}
+
+/// A [MultiSourceSpanException] that's also a [FormatException].
+class MultiSourceSpanFormatException extends MultiSourceSpanException
+ implements FormatException {
+ @override
+ final dynamic source;
+
+ @override
+ int? get offset => span?.start.offset;
+
+ MultiSourceSpanFormatException(
+ super.message, super.span, super.primaryLabel, super.secondarySpans,
+ [this.source]);
+}
diff --git a/pkgs/source_span/lib/src/span_mixin.dart b/pkgs/source_span/lib/src/span_mixin.dart
new file mode 100644
index 0000000..29b6119
--- /dev/null
+++ b/pkgs/source_span/lib/src/span_mixin.dart
@@ -0,0 +1,84 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as p;
+
+import 'highlighter.dart';
+import 'span.dart';
+import 'span_with_context.dart';
+import 'utils.dart';
+
+/// A mixin for easily implementing [SourceSpan].
+///
+/// This implements the [SourceSpan] methods in terms of [start], [end], and
+/// [text]. This assumes that [start] and [end] have the same source URL, that
+/// [start] comes before [end], and that [text] has a number of characters equal
+/// to the distance between [start] and [end].
+abstract class SourceSpanMixin implements SourceSpan {
+ @override
+ Uri? get sourceUrl => start.sourceUrl;
+
+ @override
+ int get length => end.offset - start.offset;
+
+ @override
+ int compareTo(SourceSpan other) {
+ final result = start.compareTo(other.start);
+ return result == 0 ? end.compareTo(other.end) : result;
+ }
+
+ @override
+ SourceSpan union(SourceSpan other) {
+ if (sourceUrl != other.sourceUrl) {
+ throw ArgumentError('Source URLs "$sourceUrl" and '
+ " \"${other.sourceUrl}\" don't match.");
+ }
+
+ final start = min(this.start, other.start);
+ final end = max(this.end, other.end);
+ final beginSpan = start == this.start ? this : other;
+ final endSpan = end == this.end ? this : other;
+
+ if (beginSpan.end.compareTo(endSpan.start) < 0) {
+ throw ArgumentError('Spans $this and $other are disjoint.');
+ }
+
+ final text = beginSpan.text +
+ endSpan.text.substring(beginSpan.end.distance(endSpan.start));
+ return SourceSpan(start, end, text);
+ }
+
+ @override
+ String message(String message, {Object? color}) {
+ final buffer = StringBuffer()
+ ..write('line ${start.line + 1}, column ${start.column + 1}');
+ if (sourceUrl != null) buffer.write(' of ${p.prettyUri(sourceUrl)}');
+ buffer.write(': $message');
+
+ final highlight = this.highlight(color: color);
+ if (highlight.isNotEmpty) {
+ buffer
+ ..writeln()
+ ..write(highlight);
+ }
+
+ return buffer.toString();
+ }
+
+ @override
+ String highlight({Object? color}) {
+ if (this is! SourceSpanWithContext && length == 0) return '';
+ return Highlighter(this, color: color).highlight();
+ }
+
+ @override
+ bool operator ==(Object other) =>
+ other is SourceSpan && start == other.start && end == other.end;
+
+ @override
+ int get hashCode => Object.hash(start, end);
+
+ @override
+ String toString() => '<$runtimeType: from $start to $end "$text">';
+}
diff --git a/pkgs/source_span/lib/src/span_with_context.dart b/pkgs/source_span/lib/src/span_with_context.dart
new file mode 100644
index 0000000..776c789
--- /dev/null
+++ b/pkgs/source_span/lib/src/span_with_context.dart
@@ -0,0 +1,51 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'location.dart';
+import 'span.dart';
+import 'utils.dart';
+
+/// A class that describes a segment of source text with additional context.
+class SourceSpanWithContext extends SourceSpanBase {
+ // This is a getter so that subclasses can override it.
+ /// Text around the span, which includes the line containing this span.
+ String get context => _context;
+ final String _context;
+
+ /// Creates a new span from [start] to [end] (exclusive) containing [text], in
+ /// the given [context].
+ ///
+ /// [start] and [end] must have the same source URL and [start] must come
+ /// before [end]. [text] must have a number of characters equal to the
+ /// distance between [start] and [end]. [context] must contain [text], and
+ /// [text] should start at `start.column` from the beginning of a line in
+ /// [context].
+ SourceSpanWithContext(
+ SourceLocation start, SourceLocation end, String text, this._context)
+ : super(start, end, text) {
+ if (!context.contains(text)) {
+ throw ArgumentError('The context line "$context" must contain "$text".');
+ }
+
+ if (findLineStart(context, text, start.column) == null) {
+ throw ArgumentError('The span text "$text" must start at '
+ 'column ${start.column + 1} in a line within "$context".');
+ }
+ }
+}
+
+// TODO(#52): Move these to instance methods in the next breaking release.
+/// Extension methods on the base [SourceSpan] API.
+extension SourceSpanWithContextExtension on SourceSpanWithContext {
+ /// Returns a span from [start] code units (inclusive) to [end] code units
+ /// (exclusive) after the beginning of this span.
+ SourceSpanWithContext subspan(int start, [int? end]) {
+ RangeError.checkValidRange(start, end, length);
+ if (start == 0 && (end == null || end == length)) return this;
+
+ final locations = subspanLocations(this, start, end);
+ return SourceSpanWithContext(
+ locations[0], locations[1], text.substring(start, end), context);
+ }
+}
diff --git a/pkgs/source_span/lib/src/utils.dart b/pkgs/source_span/lib/src/utils.dart
new file mode 100644
index 0000000..aba14ec
--- /dev/null
+++ b/pkgs/source_span/lib/src/utils.dart
@@ -0,0 +1,145 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'charcode.dart';
+import 'location.dart';
+import 'span.dart';
+import 'span_with_context.dart';
+
+/// Returns the minimum of [obj1] and [obj2] according to
+/// [Comparable.compareTo].
+T min<T extends Comparable<T>>(T obj1, T obj2) =>
+ obj1.compareTo(obj2) > 0 ? obj2 : obj1;
+
+/// Returns the maximum of [obj1] and [obj2] according to
+/// [Comparable.compareTo].
+T max<T extends Comparable<T>>(T obj1, T obj2) =>
+ obj1.compareTo(obj2) > 0 ? obj1 : obj2;
+
+/// Returns whether all elements of [iter] are the same value, according to
+/// `==`.
+bool isAllTheSame(Iterable<Object?> iter) {
+ if (iter.isEmpty) return true;
+ final firstValue = iter.first;
+ for (var value in iter.skip(1)) {
+ if (value != firstValue) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/// Returns whether [span] covers multiple lines.
+bool isMultiline(SourceSpan span) => span.start.line != span.end.line;
+
+/// Sets the first `null` element of [list] to [element].
+void replaceFirstNull<E>(List<E?> list, E element) {
+ final index = list.indexOf(null);
+ if (index < 0) throw ArgumentError('$list contains no null elements.');
+ list[index] = element;
+}
+
+/// Sets the element of [list] that currently contains [element] to `null`.
+void replaceWithNull<E>(List<E?> list, E element) {
+ final index = list.indexOf(element);
+ if (index < 0) {
+ throw ArgumentError('$list contains no elements matching $element.');
+ }
+
+ list[index] = null;
+}
+
+/// Returns the number of instances of [codeUnit] in [string].
+int countCodeUnits(String string, int codeUnit) {
+ var count = 0;
+ for (var codeUnitToCheck in string.codeUnits) {
+ if (codeUnitToCheck == codeUnit) count++;
+ }
+ return count;
+}
+
+/// Finds a line in [context] containing [text] at the specified [column].
+///
+/// Returns the index in [context] where that line begins, or null if none
+/// exists.
+int? findLineStart(String context, String text, int column) {
+ // If the text is empty, we just want to find the first line that has at least
+ // [column] characters.
+ if (text.isEmpty) {
+ var beginningOfLine = 0;
+ while (true) {
+ final index = context.indexOf('\n', beginningOfLine);
+ if (index == -1) {
+ return context.length - beginningOfLine >= column
+ ? beginningOfLine
+ : null;
+ }
+
+ if (index - beginningOfLine >= column) return beginningOfLine;
+ beginningOfLine = index + 1;
+ }
+ }
+
+ var index = context.indexOf(text);
+ while (index != -1) {
+ // Start looking before [index] in case [text] starts with a newline.
+ final lineStart = index == 0 ? 0 : context.lastIndexOf('\n', index - 1) + 1;
+ final textColumn = index - lineStart;
+ if (column == textColumn) return lineStart;
+ index = context.indexOf(text, index + 1);
+ }
+ // ignore: avoid_returning_null
+ return null;
+}
+
+/// Returns a two-element list containing the start and end locations of the
+/// span from [start] code units (inclusive) to [end] code units (exclusive)
+/// after the beginning of [span].
+///
+/// This is factored out so it can be shared between
+/// [SourceSpanExtension.subspan] and [SourceSpanWithContextExtension.subspan].
+List<SourceLocation> subspanLocations(SourceSpan span, int start, [int? end]) {
+ final text = span.text;
+ final startLocation = span.start;
+ var line = startLocation.line;
+ var column = startLocation.column;
+
+ // Adjust [line] and [column] as necessary if the character at [i] in [text]
+ // is a newline.
+ void consumeCodePoint(int i) {
+ final codeUnit = text.codeUnitAt(i);
+ if (codeUnit == $lf ||
+ // A carriage return counts as a newline, but only if it's not
+ // followed by a line feed.
+ (codeUnit == $cr &&
+ (i + 1 == text.length || text.codeUnitAt(i + 1) != $lf))) {
+ line += 1;
+ column = 0;
+ } else {
+ column += 1;
+ }
+ }
+
+ for (var i = 0; i < start; i++) {
+ consumeCodePoint(i);
+ }
+
+ final newStartLocation = SourceLocation(startLocation.offset + start,
+ sourceUrl: span.sourceUrl, line: line, column: column);
+
+ SourceLocation newEndLocation;
+ if (end == null || end == span.length) {
+ newEndLocation = span.end;
+ } else if (end == start) {
+ newEndLocation = newStartLocation;
+ } else {
+ for (var i = start; i < end; i++) {
+ consumeCodePoint(i);
+ }
+ newEndLocation = SourceLocation(startLocation.offset + end,
+ sourceUrl: span.sourceUrl, line: line, column: column);
+ }
+
+ return [newStartLocation, newEndLocation];
+}
diff --git a/pkgs/source_span/pubspec.yaml b/pkgs/source_span/pubspec.yaml
new file mode 100644
index 0000000..8757b2d
--- /dev/null
+++ b/pkgs/source_span/pubspec.yaml
@@ -0,0 +1,17 @@
+name: source_span
+version: 1.10.1
+description: >-
+ Provides a standard representation for source code locations and spans.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/source_span
+
+environment:
+ sdk: ^3.1.0
+
+dependencies:
+ collection: ^1.15.0
+ path: ^1.8.0
+ term_glyph: ^1.2.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
+ test: ^1.16.0
diff --git a/pkgs/source_span/test/file_test.dart b/pkgs/source_span/test/file_test.dart
new file mode 100644
index 0000000..dff51ee
--- /dev/null
+++ b/pkgs/source_span/test/file_test.dart
@@ -0,0 +1,530 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+void main() {
+ late SourceFile file;
+ setUp(() {
+ file = SourceFile.fromString('''
+foo bar baz
+whiz bang boom
+zip zap zop''', url: 'foo.dart');
+ });
+
+ group('errors', () {
+ group('for span()', () {
+ test('end must come after start', () {
+ expect(() => file.span(10, 5), throwsArgumentError);
+ });
+
+ test('start may not be negative', () {
+ expect(() => file.span(-1, 5), throwsRangeError);
+ });
+
+ test('end may not be outside the file', () {
+ expect(() => file.span(10, 100), throwsRangeError);
+ });
+ });
+
+ group('for location()', () {
+ test('offset may not be negative', () {
+ expect(() => file.location(-1), throwsRangeError);
+ });
+
+ test('offset may not be outside the file', () {
+ expect(() => file.location(100), throwsRangeError);
+ });
+ });
+
+ group('for getLine()', () {
+ test('offset may not be negative', () {
+ expect(() => file.getLine(-1), throwsRangeError);
+ });
+
+ test('offset may not be outside the file', () {
+ expect(() => file.getLine(100), throwsRangeError);
+ });
+ });
+
+ group('for getColumn()', () {
+ test('offset may not be negative', () {
+ expect(() => file.getColumn(-1), throwsRangeError);
+ });
+
+ test('offset may not be outside the file', () {
+ expect(() => file.getColumn(100), throwsRangeError);
+ });
+
+ test('line may not be negative', () {
+ expect(() => file.getColumn(1, line: -1), throwsRangeError);
+ });
+
+ test('line may not be outside the file', () {
+ expect(() => file.getColumn(1, line: 100), throwsRangeError);
+ });
+
+ test('line must be accurate', () {
+ expect(() => file.getColumn(1, line: 1), throwsRangeError);
+ });
+ });
+
+ group('getOffset()', () {
+ test('line may not be negative', () {
+ expect(() => file.getOffset(-1), throwsRangeError);
+ });
+
+ test('column may not be negative', () {
+ expect(() => file.getOffset(1, -1), throwsRangeError);
+ });
+
+ test('line may not be outside the file', () {
+ expect(() => file.getOffset(100), throwsRangeError);
+ });
+
+ test('column may not be outside the file', () {
+ expect(() => file.getOffset(2, 100), throwsRangeError);
+ });
+
+ test('column may not be outside the line', () {
+ expect(() => file.getOffset(1, 20), throwsRangeError);
+ });
+ });
+
+ group('for getText()', () {
+ test('end must come after start', () {
+ expect(() => file.getText(10, 5), throwsArgumentError);
+ });
+
+ test('start may not be negative', () {
+ expect(() => file.getText(-1, 5), throwsRangeError);
+ });
+
+ test('end may not be outside the file', () {
+ expect(() => file.getText(10, 100), throwsRangeError);
+ });
+ });
+
+ group('for span().union()', () {
+ test('source URLs must match', () {
+ final other = SourceSpan(SourceLocation(10), SourceLocation(11), '_');
+
+ expect(() => file.span(9, 10).union(other), throwsArgumentError);
+ });
+
+ test('spans may not be disjoint', () {
+ expect(() => file.span(9, 10).union(file.span(11, 12)),
+ throwsArgumentError);
+ });
+ });
+
+ test('for span().expand() source URLs must match', () {
+ final other = SourceFile.fromString('''
+foo bar baz
+whiz bang boom
+zip zap zop''', url: 'bar.dart').span(10, 11);
+
+ expect(() => file.span(9, 10).expand(other), throwsArgumentError);
+ });
+ });
+
+ test('fields work correctly', () {
+ expect(file.url, equals(Uri.parse('foo.dart')));
+ expect(file.lines, equals(3));
+ expect(file.length, equals(38));
+ });
+
+ group('new SourceFile()', () {
+ test('handles CRLF correctly', () {
+ expect(SourceFile.fromString('foo\r\nbar').getLine(6), equals(1));
+ });
+
+ test('handles a lone CR correctly', () {
+ expect(SourceFile.fromString('foo\rbar').getLine(5), equals(1));
+ });
+ });
+
+ group('span()', () {
+ test('returns a span between the given offsets', () {
+ final span = file.span(5, 10);
+ expect(span.start, equals(file.location(5)));
+ expect(span.end, equals(file.location(10)));
+ });
+
+ test('end defaults to the end of the file', () {
+ final span = file.span(5);
+ expect(span.start, equals(file.location(5)));
+ expect(span.end, equals(file.location(file.length)));
+ });
+ });
+
+ group('getLine()', () {
+ test('works for a middle character on the line', () {
+ expect(file.getLine(15), equals(1));
+ });
+
+ test('works for the first character of a line', () {
+ expect(file.getLine(12), equals(1));
+ });
+
+ test('works for a newline character', () {
+ expect(file.getLine(11), equals(0));
+ });
+
+ test('works for the last offset', () {
+ expect(file.getLine(file.length), equals(2));
+ });
+ });
+
+ group('getColumn()', () {
+ test('works for a middle character on the line', () {
+ expect(file.getColumn(15), equals(3));
+ });
+
+ test('works for the first character of a line', () {
+ expect(file.getColumn(12), equals(0));
+ });
+
+ test('works for a newline character', () {
+ expect(file.getColumn(11), equals(11));
+ });
+
+ test('works when line is passed as well', () {
+ expect(file.getColumn(12, line: 1), equals(0));
+ });
+
+ test('works for the last offset', () {
+ expect(file.getColumn(file.length), equals(11));
+ });
+ });
+
+ group('getOffset()', () {
+ test('works for a middle character on the line', () {
+ expect(file.getOffset(1, 3), equals(15));
+ });
+
+ test('works for the first character of a line', () {
+ expect(file.getOffset(1), equals(12));
+ });
+
+ test('works for a newline character', () {
+ expect(file.getOffset(0, 11), equals(11));
+ });
+
+ test('works for the last offset', () {
+ expect(file.getOffset(2, 11), equals(file.length));
+ });
+ });
+
+ group('getText()', () {
+ test('returns a substring of the source', () {
+ expect(file.getText(8, 15), equals('baz\nwhi'));
+ });
+
+ test('end defaults to the end of the file', () {
+ expect(file.getText(20), equals('g boom\nzip zap zop'));
+ });
+ });
+
+ group('FileLocation', () {
+ test('reports the correct line number', () {
+ expect(file.location(15).line, equals(1));
+ });
+
+ test('reports the correct column number', () {
+ expect(file.location(15).column, equals(3));
+ });
+
+ test('pointSpan() returns a FileSpan', () {
+ final location = file.location(15);
+ final span = location.pointSpan();
+ expect(span, isA<FileSpan>());
+ expect(span.start, equals(location));
+ expect(span.end, equals(location));
+ expect(span.text, isEmpty);
+ });
+ });
+
+ group('FileSpan', () {
+ test('text returns a substring of the source', () {
+ expect(file.span(8, 15).text, equals('baz\nwhi'));
+ });
+
+ test('text includes the last char when end is defaulted to EOF', () {
+ expect(file.span(29).text, equals('p zap zop'));
+ });
+
+ group('context', () {
+ test("contains the span's text", () {
+ final span = file.span(8, 15);
+ expect(span.context.contains(span.text), isTrue);
+ expect(span.context, equals('foo bar baz\nwhiz bang boom\n'));
+ });
+
+ test('contains the previous line for a point span at the end of a line',
+ () {
+ final span = file.span(25, 25);
+ expect(span.context, equals('whiz bang boom\n'));
+ });
+
+ test('contains the next line for a point span at the beginning of a line',
+ () {
+ final span = file.span(12, 12);
+ expect(span.context, equals('whiz bang boom\n'));
+ });
+
+ group('for a point span at the end of a file', () {
+ test('without a newline, contains the last line', () {
+ final span = file.span(file.length, file.length);
+ expect(span.context, equals('zip zap zop'));
+ });
+
+ test('with a newline, contains an empty line', () {
+ file = SourceFile.fromString('''
+foo bar baz
+whiz bang boom
+zip zap zop
+''', url: 'foo.dart');
+
+ final span = file.span(file.length, file.length);
+ expect(span.context, isEmpty);
+ });
+ });
+ });
+
+ group('union()', () {
+ late FileSpan span;
+ setUp(() {
+ span = file.span(5, 12);
+ });
+
+ test('works with a preceding adjacent span', () {
+ final other = file.span(0, 5);
+ final result = span.union(other);
+ expect(result.start, equals(other.start));
+ expect(result.end, equals(span.end));
+ expect(result.text, equals('foo bar baz\n'));
+ });
+
+ test('works with a preceding overlapping span', () {
+ final other = file.span(0, 8);
+ final result = span.union(other);
+ expect(result.start, equals(other.start));
+ expect(result.end, equals(span.end));
+ expect(result.text, equals('foo bar baz\n'));
+ });
+
+ test('works with a following adjacent span', () {
+ final other = file.span(12, 16);
+ final result = span.union(other);
+ expect(result.start, equals(span.start));
+ expect(result.end, equals(other.end));
+ expect(result.text, equals('ar baz\nwhiz'));
+ });
+
+ test('works with a following overlapping span', () {
+ final other = file.span(9, 16);
+ final result = span.union(other);
+ expect(result.start, equals(span.start));
+ expect(result.end, equals(other.end));
+ expect(result.text, equals('ar baz\nwhiz'));
+ });
+
+ test('works with an internal overlapping span', () {
+ final other = file.span(7, 10);
+ expect(span.union(other), equals(span));
+ });
+
+ test('works with an external overlapping span', () {
+ final other = file.span(0, 16);
+ expect(span.union(other), equals(other));
+ });
+
+ test('returns a FileSpan for a FileSpan input', () {
+ expect(span.union(file.span(0, 5)), isA<FileSpan>());
+ });
+
+ test('returns a base SourceSpan for a SourceSpan input', () {
+ final other = SourceSpan(SourceLocation(0, sourceUrl: 'foo.dart'),
+ SourceLocation(5, sourceUrl: 'foo.dart'), 'hey, ');
+ final result = span.union(other);
+ expect(result, isNot(isA<FileSpan>()));
+ expect(result.start, equals(other.start));
+ expect(result.end, equals(span.end));
+ expect(result.text, equals('hey, ar baz\n'));
+ });
+ });
+
+ group('expand()', () {
+ late FileSpan span;
+ setUp(() {
+ span = file.span(5, 12);
+ });
+
+ test('works with a preceding nonadjacent span', () {
+ final other = file.span(0, 3);
+ final result = span.expand(other);
+ expect(result.start, equals(other.start));
+ expect(result.end, equals(span.end));
+ expect(result.text, equals('foo bar baz\n'));
+ });
+
+ test('works with a preceding overlapping span', () {
+ final other = file.span(0, 8);
+ final result = span.expand(other);
+ expect(result.start, equals(other.start));
+ expect(result.end, equals(span.end));
+ expect(result.text, equals('foo bar baz\n'));
+ });
+
+ test('works with a following nonadjacent span', () {
+ final other = file.span(14, 16);
+ final result = span.expand(other);
+ expect(result.start, equals(span.start));
+ expect(result.end, equals(other.end));
+ expect(result.text, equals('ar baz\nwhiz'));
+ });
+
+ test('works with a following overlapping span', () {
+ final other = file.span(9, 16);
+ final result = span.expand(other);
+ expect(result.start, equals(span.start));
+ expect(result.end, equals(other.end));
+ expect(result.text, equals('ar baz\nwhiz'));
+ });
+
+ test('works with an internal overlapping span', () {
+ final other = file.span(7, 10);
+ expect(span.expand(other), equals(span));
+ });
+
+ test('works with an external overlapping span', () {
+ final other = file.span(0, 16);
+ expect(span.expand(other), equals(other));
+ });
+ });
+
+ group('subspan()', () {
+ late FileSpan span;
+ setUp(() {
+ span = file.span(5, 11); // "ar baz"
+ });
+
+ group('errors', () {
+ test('start must be greater than zero', () {
+ expect(() => span.subspan(-1), throwsRangeError);
+ });
+
+ test('start must be less than or equal to length', () {
+ expect(() => span.subspan(span.length + 1), throwsRangeError);
+ });
+
+ test('end must be greater than start', () {
+ expect(() => span.subspan(2, 1), throwsRangeError);
+ });
+
+ test('end must be less than or equal to length', () {
+ expect(() => span.subspan(0, span.length + 1), throwsRangeError);
+ });
+ });
+
+ test('preserves the source URL', () {
+ final result = span.subspan(1, 2);
+ expect(result.start.sourceUrl, equals(span.sourceUrl));
+ expect(result.end.sourceUrl, equals(span.sourceUrl));
+ });
+
+ group('returns the original span', () {
+ test('with an implicit end',
+ () => expect(span.subspan(0), equals(span)));
+
+ test('with an explicit end',
+ () => expect(span.subspan(0, span.length), equals(span)));
+ });
+
+ group('within a single line', () {
+ test('returns a strict substring of the original span', () {
+ final result = span.subspan(1, 5);
+ expect(result.text, equals('r ba'));
+ expect(result.start.offset, equals(6));
+ expect(result.start.line, equals(0));
+ expect(result.start.column, equals(6));
+ expect(result.end.offset, equals(10));
+ expect(result.end.line, equals(0));
+ expect(result.end.column, equals(10));
+ });
+
+ test('an implicit end goes to the end of the original span', () {
+ final result = span.subspan(1);
+ expect(result.text, equals('r baz'));
+ expect(result.start.offset, equals(6));
+ expect(result.start.line, equals(0));
+ expect(result.start.column, equals(6));
+ expect(result.end.offset, equals(11));
+ expect(result.end.line, equals(0));
+ expect(result.end.column, equals(11));
+ });
+
+ test('can return an empty span', () {
+ final result = span.subspan(3, 3);
+ expect(result.text, isEmpty);
+ expect(result.start.offset, equals(8));
+ expect(result.start.line, equals(0));
+ expect(result.start.column, equals(8));
+ expect(result.end, equals(result.start));
+ });
+ });
+
+ group('across multiple lines', () {
+ setUp(() {
+ span = file.span(22, 30); // "boom\nzip"
+ });
+
+ test('with start and end in the middle of a line', () {
+ final result = span.subspan(3, 6);
+ expect(result.text, equals('m\nz'));
+ expect(result.start.offset, equals(25));
+ expect(result.start.line, equals(1));
+ expect(result.start.column, equals(13));
+ expect(result.end.offset, equals(28));
+ expect(result.end.line, equals(2));
+ expect(result.end.column, equals(1));
+ });
+
+ test('with start at the end of a line', () {
+ final result = span.subspan(4, 6);
+ expect(result.text, equals('\nz'));
+ expect(result.start.offset, equals(26));
+ expect(result.start.line, equals(1));
+ expect(result.start.column, equals(14));
+ });
+
+ test('with start at the beginning of a line', () {
+ final result = span.subspan(5, 6);
+ expect(result.text, equals('z'));
+ expect(result.start.offset, equals(27));
+ expect(result.start.line, equals(2));
+ expect(result.start.column, equals(0));
+ });
+
+ test('with end at the end of a line', () {
+ final result = span.subspan(3, 4);
+ expect(result.text, equals('m'));
+ expect(result.end.offset, equals(26));
+ expect(result.end.line, equals(1));
+ expect(result.end.column, equals(14));
+ });
+
+ test('with end at the beginning of a line', () {
+ final result = span.subspan(3, 5);
+ expect(result.text, equals('m\n'));
+ expect(result.end.offset, equals(27));
+ expect(result.end.line, equals(2));
+ expect(result.end.column, equals(0));
+ });
+ });
+ });
+ });
+}
diff --git a/pkgs/source_span/test/highlight_test.dart b/pkgs/source_span/test/highlight_test.dart
new file mode 100644
index 0000000..93c42db
--- /dev/null
+++ b/pkgs/source_span/test/highlight_test.dart
@@ -0,0 +1,605 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: prefer_interpolation_to_compose_strings
+
+import 'package:source_span/source_span.dart';
+import 'package:source_span/src/colors.dart' as colors;
+import 'package:term_glyph/term_glyph.dart' as glyph;
+import 'package:test/test.dart';
+
+void main() {
+ late bool oldAscii;
+ setUpAll(() {
+ oldAscii = glyph.ascii;
+ glyph.ascii = true;
+ });
+
+ tearDownAll(() {
+ glyph.ascii = oldAscii;
+ });
+
+ late SourceFile file;
+ setUp(() {
+ file = SourceFile.fromString('''
+foo bar baz
+whiz bang boom
+zip zap zop
+''');
+ });
+
+ test('points to the span in the source', () {
+ expect(file.span(4, 7).highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ^^^
+ '"""));
+ });
+
+ test('gracefully handles a missing source URL', () {
+ final span = SourceFile.fromString('foo bar baz').span(4, 7);
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ^^^
+ '"""));
+ });
+
+ group('highlights a point span', () {
+ test('in the middle of a line', () {
+ expect(file.location(4).pointSpan().highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ^
+ '"""));
+ });
+
+ test('at the beginning of the file', () {
+ expect(file.location(0).pointSpan().highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ^
+ '"""));
+ });
+
+ test('at the beginning of a line', () {
+ expect(file.location(12).pointSpan().highlight(), equals("""
+ ,
+2 | whiz bang boom
+ | ^
+ '"""));
+ });
+
+ test('at the end of a line', () {
+ expect(file.location(11).pointSpan().highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ^
+ '"""));
+ });
+
+ test('at the end of the file', () {
+ expect(file.location(38).pointSpan().highlight(), equals("""
+ ,
+3 | zip zap zop
+ | ^
+ '"""));
+ });
+
+ test('after the end of the file', () {
+ expect(file.location(39).pointSpan().highlight(), equals("""
+ ,
+4 |
+ | ^
+ '"""));
+ });
+
+ test('at the end of the file with no trailing newline', () {
+ file = SourceFile.fromString('zip zap zop');
+ expect(file.location(10).pointSpan().highlight(), equals("""
+ ,
+1 | zip zap zop
+ | ^
+ '"""));
+ });
+
+ test('after the end of the file with no trailing newline', () {
+ file = SourceFile.fromString('zip zap zop');
+ expect(file.location(11).pointSpan().highlight(), equals("""
+ ,
+1 | zip zap zop
+ | ^
+ '"""));
+ });
+
+ test('in an empty file', () {
+ expect(SourceFile.fromString('').location(0).pointSpan().highlight(),
+ equals("""
+ ,
+1 |
+ | ^
+ '"""));
+ });
+
+ test('on an empty line', () {
+ final file = SourceFile.fromString('foo\n\nbar');
+ expect(file.location(4).pointSpan().highlight(), equals("""
+ ,
+2 |
+ | ^
+ '"""));
+ });
+ });
+
+ test('highlights a single-line file without a newline', () {
+ expect(SourceFile.fromString('foo bar').span(0, 7).highlight(), equals("""
+ ,
+1 | foo bar
+ | ^^^^^^^
+ '"""));
+ });
+
+ test('highlights text including a trailing newline', () {
+ expect(file.span(8, 12).highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ^^^
+ '"""));
+ });
+
+ test('highlights a single empty line', () {
+ expect(
+ SourceFile.fromString('foo\n\nbar').span(4, 5).highlight(), equals("""
+ ,
+2 |
+ | ^
+ '"""));
+ });
+
+ test('highlights a trailing newline', () {
+ expect(file.span(11, 12).highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ^
+ '"""));
+ });
+
+ group('with a multiline span', () {
+ test('highlights the middle of the first and last lines', () {
+ expect(file.span(4, 34).highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+3 | | zip zap zop
+ | '-------^
+ '"""));
+ });
+
+ test('works when it begins at the end of a line', () {
+ expect(file.span(11, 34).highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,------------^
+2 | | whiz bang boom
+3 | | zip zap zop
+ | '-------^
+ '"""));
+ });
+
+ test('works when it ends at the beginning of a line', () {
+ expect(file.span(4, 28).highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+3 | | zip zap zop
+ | '-^
+ '"""));
+ });
+
+ test('highlights the full first line', () {
+ expect(file.span(0, 34).highlight(), equals("""
+ ,
+1 | / foo bar baz
+2 | | whiz bang boom
+3 | | zip zap zop
+ | '-------^
+ '"""));
+ });
+
+ test("highlights the full first line even if it's indented", () {
+ final file = SourceFile.fromString('''
+ foo bar baz
+ whiz bang boom
+ zip zap zop
+''');
+
+ expect(file.span(2, 38).highlight(), equals("""
+ ,
+1 | / foo bar baz
+2 | | whiz bang boom
+3 | | zip zap zop
+ | '-------^
+ '"""));
+ });
+
+ test("highlights the full first line if it's empty", () {
+ final file = SourceFile.fromString('''
+foo
+
+bar
+''');
+
+ expect(file.span(4, 9).highlight(), equals(r"""
+ ,
+2 | /
+3 | \ bar
+ '"""));
+ });
+
+ test('highlights the full last line', () {
+ expect(file.span(4, 27).highlight(), equals(r"""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | \ whiz bang boom
+ '"""));
+ });
+
+ test('highlights the full last line with no trailing newline', () {
+ expect(file.span(4, 26).highlight(), equals(r"""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | \ whiz bang boom
+ '"""));
+ });
+
+ test('highlights the full last line with a trailing Windows newline', () {
+ final file = SourceFile.fromString('''
+foo bar baz\r
+whiz bang boom\r
+zip zap zop\r
+''');
+
+ expect(file.span(4, 29).highlight(), equals(r"""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | \ whiz bang boom
+ '"""));
+ });
+
+ test('highlights the full last line at the end of the file', () {
+ expect(file.span(4, 39).highlight(), equals(r"""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+3 | \ zip zap zop
+ '"""));
+ });
+
+ test(
+ 'highlights the full last line at the end of the file with no trailing '
+ 'newline', () {
+ final file = SourceFile.fromString('''
+foo bar baz
+whiz bang boom
+zip zap zop''');
+
+ expect(file.span(4, 38).highlight(), equals(r"""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+3 | \ zip zap zop
+ '"""));
+ });
+
+ test("highlights the full last line if it's empty", () {
+ final file = SourceFile.fromString('''
+foo
+
+bar
+''');
+
+ expect(file.span(0, 5).highlight(), equals(r"""
+ ,
+1 | / foo
+2 | \
+ '"""));
+ });
+
+ test('highlights multiple empty lines', () {
+ final file = SourceFile.fromString('foo\n\n\n\nbar');
+ expect(file.span(4, 7).highlight(), equals(r"""
+ ,
+2 | /
+3 | |
+4 | \
+ '"""));
+ });
+
+ // Regression test for #32
+ test('highlights the end of a line and an empty line', () {
+ final file = SourceFile.fromString('foo\n\n');
+ expect(file.span(3, 5).highlight(), equals(r"""
+ ,
+1 | foo
+ | ,----^
+2 | \
+ '"""));
+ });
+ });
+
+ group('prints tabs as spaces', () {
+ group('in a single-line span', () {
+ test('before the highlighted section', () {
+ final span = SourceFile.fromString('foo\tbar baz').span(4, 7);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ^^^
+ '"""));
+ });
+
+ test('within the highlighted section', () {
+ final span = SourceFile.fromString('foo bar\tbaz bang').span(4, 11);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz bang
+ | ^^^^^^^^^^
+ '"""));
+ });
+
+ test('after the highlighted section', () {
+ final span = SourceFile.fromString('foo bar\tbaz').span(4, 7);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ^^^
+ '"""));
+ });
+ });
+
+ group('in a multi-line span', () {
+ test('before the highlighted section', () {
+ final span = SourceFile.fromString('''
+foo\tbar baz
+whiz bang boom
+''').span(4, 21);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,--------^
+2 | | whiz bang boom
+ | '---------^
+ '"""));
+ });
+
+ test('within the first highlighted line', () {
+ final span = SourceFile.fromString('''
+foo bar\tbaz
+whiz bang boom
+''').span(4, 21);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+ | '---------^
+ '"""));
+ });
+
+ test('at the beginning of the first highlighted line', () {
+ final span = SourceFile.fromString('''
+foo bar\tbaz
+whiz bang boom
+''').span(7, 21);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,--------^
+2 | | whiz bang boom
+ | '---------^
+ '"""));
+ });
+
+ test('within a middle highlighted line', () {
+ final span = SourceFile.fromString('''
+foo bar baz
+whiz\tbang boom
+zip zap zop
+''').span(4, 34);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+3 | | zip zap zop
+ | '-------^
+ '"""));
+ });
+
+ test('within the last highlighted line', () {
+ final span = SourceFile.fromString('''
+foo bar baz
+whiz\tbang boom
+''').span(4, 21);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+ | '------------^
+ '"""));
+ });
+
+ test('at the end of the last highlighted line', () {
+ final span = SourceFile.fromString('''
+foo bar baz
+whiz\tbang boom
+''').span(4, 17);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+ | '--------^
+ '"""));
+ });
+
+ test('after the highlighted section', () {
+ final span = SourceFile.fromString('''
+foo bar baz
+whiz bang\tboom
+''').span(4, 21);
+
+ expect(span.highlight(), equals("""
+ ,
+1 | foo bar baz
+ | ,-----^
+2 | | whiz bang boom
+ | '---------^
+ '"""));
+ });
+ });
+ });
+
+ group('supports lines of preceding and following context for a span', () {
+ test('within a single line', () {
+ final span = SourceSpanWithContext(
+ SourceLocation(20, line: 2, column: 5, sourceUrl: 'foo.dart'),
+ SourceLocation(27, line: 2, column: 12, sourceUrl: 'foo.dart'),
+ 'foo bar',
+ 'previous\nlines\n-----foo bar-----\nfollowing line\n');
+
+ expect(span.highlight(), equals("""
+ ,
+1 | previous
+2 | lines
+3 | -----foo bar-----
+ | ^^^^^^^
+4 | following line
+ '"""));
+ });
+
+ test('covering a full line', () {
+ final span = SourceSpanWithContext(
+ SourceLocation(15, line: 2, column: 0, sourceUrl: 'foo.dart'),
+ SourceLocation(33, line: 3, column: 0, sourceUrl: 'foo.dart'),
+ '-----foo bar-----\n',
+ 'previous\nlines\n-----foo bar-----\nfollowing line\n');
+
+ expect(span.highlight(), equals("""
+ ,
+1 | previous
+2 | lines
+3 | -----foo bar-----
+ | ^^^^^^^^^^^^^^^^^
+4 | following line
+ '"""));
+ });
+
+ test('covering multiple full lines', () {
+ final span = SourceSpanWithContext(
+ SourceLocation(15, line: 2, column: 0, sourceUrl: 'foo.dart'),
+ SourceLocation(23, line: 4, column: 0, sourceUrl: 'foo.dart'),
+ 'foo\nbar\n',
+ 'previous\nlines\nfoo\nbar\nfollowing line\n');
+
+ expect(span.highlight(), equals(r"""
+ ,
+1 | previous
+2 | lines
+3 | / foo
+4 | \ bar
+5 | following line
+ '"""));
+ });
+ });
+
+ group('colors', () {
+ test("doesn't colorize if color is false", () {
+ expect(file.span(4, 7).highlight(color: false), equals("""
+ ,
+1 | foo bar baz
+ | ^^^
+ '"""));
+ });
+
+ test('colorizes if color is true', () {
+ expect(file.span(4, 7).highlight(color: true), equals('''
+${colors.blue} ,${colors.none}
+${colors.blue}1 |${colors.none} foo ${colors.red}bar${colors.none} baz
+${colors.blue} |${colors.none} ${colors.red} ^^^${colors.none}
+${colors.blue} '${colors.none}'''));
+ });
+
+ test("uses the given color if it's passed", () {
+ expect(file.span(4, 7).highlight(color: colors.yellow), equals('''
+${colors.blue} ,${colors.none}
+${colors.blue}1 |${colors.none} foo ${colors.yellow}bar${colors.none} baz
+${colors.blue} |${colors.none} ${colors.yellow} ^^^${colors.none}
+${colors.blue} '${colors.none}'''));
+ });
+
+ test('colorizes a multiline span', () {
+ expect(file.span(4, 34).highlight(color: true), equals('''
+${colors.blue} ,${colors.none}
+${colors.blue}1 |${colors.none} foo ${colors.red}bar baz${colors.none}
+${colors.blue} |${colors.none} ${colors.red},${colors.none}${colors.red}-----^${colors.none}
+${colors.blue}2 |${colors.none} ${colors.red}|${colors.none} ${colors.red}whiz bang boom${colors.none}
+${colors.blue}3 |${colors.none} ${colors.red}|${colors.none} ${colors.red}zip zap${colors.none} zop
+${colors.blue} |${colors.none} ${colors.red}'${colors.none}${colors.red}-------^${colors.none}
+${colors.blue} '${colors.none}'''));
+ });
+
+ test('colorizes a multiline span that highlights full lines', () {
+ expect(file.span(0, 39).highlight(color: true), equals('''
+${colors.blue} ,${colors.none}
+${colors.blue}1 |${colors.none} ${colors.red}/${colors.none} ${colors.red}foo bar baz${colors.none}
+${colors.blue}2 |${colors.none} ${colors.red}|${colors.none} ${colors.red}whiz bang boom${colors.none}
+${colors.blue}3 |${colors.none} ${colors.red}\\${colors.none} ${colors.red}zip zap zop${colors.none}
+${colors.blue} '${colors.none}'''));
+ });
+ });
+
+ group('line numbers have appropriate padding', () {
+ test('with line number 9', () {
+ expect(
+ SourceFile.fromString('\n' * 8 + 'foo bar baz\n')
+ .span(8, 11)
+ .highlight(),
+ equals("""
+ ,
+9 | foo bar baz
+ | ^^^
+ '"""));
+ });
+
+ test('with line number 10', () {
+ expect(
+ SourceFile.fromString('\n' * 9 + 'foo bar baz\n')
+ .span(9, 12)
+ .highlight(),
+ equals("""
+ ,
+10 | foo bar baz
+ | ^^^
+ '"""));
+ });
+ });
+}
diff --git a/pkgs/source_span/test/location_test.dart b/pkgs/source_span/test/location_test.dart
new file mode 100644
index 0000000..bbe259b
--- /dev/null
+++ b/pkgs/source_span/test/location_test.dart
@@ -0,0 +1,97 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_span/source_span.dart';
+import 'package:test/test.dart';
+
+void main() {
+ late SourceLocation location;
+ setUp(() {
+ location = SourceLocation(15, line: 2, column: 6, sourceUrl: 'foo.dart');
+ });
+
+ group('errors', () {
+ group('for new SourceLocation()', () {
+ test('offset may not be negative', () {
+ expect(() => SourceLocation(-1), throwsRangeError);
+ });
+
+ test('line may not be negative', () {
+ expect(() => SourceLocation(0, line: -1), throwsRangeError);
+ });
+
+ test('column may not be negative', () {
+ expect(() => SourceLocation(0, column: -1), throwsRangeError);
+ });
+ });
+
+ test('for distance() source URLs must match', () {
+ expect(() => location.distance(SourceLocation(0)), throwsArgumentError);
+ });
+
+ test('for compareTo() source URLs must match', () {
+ expect(() => location.compareTo(SourceLocation(0)), throwsArgumentError);
+ });
+ });
+
+ test('fields work correctly', () {
+ expect(location.sourceUrl, equals(Uri.parse('foo.dart')));
+ expect(location.offset, equals(15));
+ expect(location.line, equals(2));
+ expect(location.column, equals(6));
+ });
+
+ group('toolString', () {
+ test('returns a computer-readable representation', () {
+ expect(location.toolString, equals('foo.dart:3:7'));
+ });
+
+ test('gracefully handles a missing source URL', () {
+ final location = SourceLocation(15, line: 2, column: 6);
+ expect(location.toolString, equals('unknown source:3:7'));
+ });
+ });
+
+ test('distance returns the absolute distance between locations', () {
+ final other = SourceLocation(10, sourceUrl: 'foo.dart');
+ expect(location.distance(other), equals(5));
+ expect(other.distance(location), equals(5));
+ });
+
+ test('pointSpan returns an empty span at location', () {
+ final span = location.pointSpan();
+ expect(span.start, equals(location));
+ expect(span.end, equals(location));
+ expect(span.text, isEmpty);
+ });
+
+ group('compareTo()', () {
+ test('sorts by offset', () {
+ final other = SourceLocation(20, sourceUrl: 'foo.dart');
+ expect(location.compareTo(other), lessThan(0));
+ expect(other.compareTo(location), greaterThan(0));
+ });
+
+ test('considers equal locations equal', () {
+ expect(location.compareTo(location), equals(0));
+ });
+ });
+
+ group('equality', () {
+ test('two locations with the same offset and source are equal', () {
+ final other = SourceLocation(15, sourceUrl: 'foo.dart');
+ expect(location, equals(other));
+ });
+
+ test("a different offset isn't equal", () {
+ final other = SourceLocation(10, sourceUrl: 'foo.dart');
+ expect(location, isNot(equals(other)));
+ });
+
+ test("a different source isn't equal", () {
+ final other = SourceLocation(15, sourceUrl: 'bar.dart');
+ expect(location, isNot(equals(other)));
+ });
+ });
+}
diff --git a/pkgs/source_span/test/multiple_highlight_test.dart b/pkgs/source_span/test/multiple_highlight_test.dart
new file mode 100644
index 0000000..139d53c
--- /dev/null
+++ b/pkgs/source_span/test/multiple_highlight_test.dart
@@ -0,0 +1,423 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_span/source_span.dart';
+import 'package:term_glyph/term_glyph.dart' as glyph;
+import 'package:test/test.dart';
+
+void main() {
+ late bool oldAscii;
+ setUpAll(() {
+ oldAscii = glyph.ascii;
+ glyph.ascii = true;
+ });
+
+ tearDownAll(() {
+ glyph.ascii = oldAscii;
+ });
+
+ late SourceFile file;
+ setUp(() {
+ file = SourceFile.fromString('''
+foo bar baz
+whiz bang boom
+zip zap zop
+fwee fwoo fwip
+argle bargle boo
+gibble bibble bop
+''', url: 'file1.txt');
+ });
+
+ test('highlights spans on separate lines', () {
+ expect(
+ file.span(17, 21).highlightMultiple(
+ 'one', {file.span(31, 34): 'two', file.span(4, 7): 'three'}),
+ equals("""
+ ,
+1 | foo bar baz
+ | === three
+2 | whiz bang boom
+ | ^^^^ one
+3 | zip zap zop
+ | === two
+ '"""));
+ });
+
+ test('highlights spans on the same line', () {
+ expect(
+ file.span(17, 21).highlightMultiple(
+ 'one', {file.span(22, 26): 'two', file.span(12, 16): 'three'}),
+ equals("""
+ ,
+2 | whiz bang boom
+ | ^^^^ one
+ | ==== three
+ | ==== two
+ '"""));
+ });
+
+ test('highlights overlapping spans on the same line', () {
+ expect(
+ file.span(17, 21).highlightMultiple(
+ 'one', {file.span(20, 26): 'two', file.span(12, 18): 'three'}),
+ equals("""
+ ,
+2 | whiz bang boom
+ | ^^^^ one
+ | ====== three
+ | ====== two
+ '"""));
+ });
+
+ test('highlights multiple multiline spans', () {
+ expect(
+ file.span(27, 54).highlightMultiple(
+ 'one', {file.span(54, 89): 'two', file.span(0, 27): 'three'}),
+ equals("""
+ ,
+1 | / foo bar baz
+2 | | whiz bang boom
+ | '--- three
+3 | / zip zap zop
+4 | | fwee fwoo fwip
+ | '--- one
+5 | / argle bargle boo
+6 | | gibble bibble bop
+ | '--- two
+ '"""));
+ });
+
+ test('highlights multiple overlapping multiline spans', () {
+ expect(
+ file.span(12, 70).highlightMultiple(
+ 'one', {file.span(54, 89): 'two', file.span(0, 27): 'three'}),
+ equals("""
+ ,
+1 | /- foo bar baz
+2 | |/ whiz bang boom
+ | '+--- three
+3 | | zip zap zop
+4 | | fwee fwoo fwip
+5 | /+ argle bargle boo
+ | |'--- one
+6 | | gibble bibble bop
+ | '---- two
+ '"""));
+ });
+
+ test('highlights many layers of overlaps', () {
+ expect(
+ file.span(0, 54).highlightMultiple('one', {
+ file.span(12, 77): 'two',
+ file.span(27, 84): 'three',
+ file.span(39, 88): 'four'
+ }),
+ equals("""
+ ,
+1 | /--- foo bar baz
+2 | |/-- whiz bang boom
+3 | ||/- zip zap zop
+4 | |||/ fwee fwoo fwip
+ | '+++--- one
+5 | ||| argle bargle boo
+6 | ||| gibble bibble bop
+ | '++------^ two
+ | '+-------------^ three
+ | '--- four
+ '"""));
+ });
+
+ group("highlights a multiline span that's a subset", () {
+ test('with no first or last line overlap', () {
+ expect(
+ file
+ .span(27, 53)
+ .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+ equals("""
+ ,
+2 | /- whiz bang boom
+3 | |/ zip zap zop
+4 | || fwee fwoo fwip
+ | |'--- inner
+5 | | argle bargle boo
+ | '---- outer
+ '"""));
+ });
+
+ test('overlapping the whole first line', () {
+ expect(
+ file
+ .span(12, 53)
+ .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+ equals("""
+ ,
+2 | // whiz bang boom
+3 | || zip zap zop
+4 | || fwee fwoo fwip
+ | |'--- inner
+5 | | argle bargle boo
+ | '---- outer
+ '"""));
+ });
+
+ test('overlapping part of first line', () {
+ expect(
+ file
+ .span(17, 53)
+ .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+ equals("""
+ ,
+2 | /- whiz bang boom
+ | |,------^
+3 | || zip zap zop
+4 | || fwee fwoo fwip
+ | |'--- inner
+5 | | argle bargle boo
+ | '---- outer
+ '"""));
+ });
+
+ test('overlapping the whole last line', () {
+ expect(
+ file
+ .span(27, 70)
+ .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+ equals("""
+ ,
+2 | /- whiz bang boom
+3 | |/ zip zap zop
+4 | || fwee fwoo fwip
+5 | || argle bargle boo
+ | |'--- inner
+ | '---- outer
+ '"""));
+ });
+
+ test('overlapping part of the last line', () {
+ expect(
+ file
+ .span(27, 66)
+ .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+ equals("""
+ ,
+2 | /- whiz bang boom
+3 | |/ zip zap zop
+4 | || fwee fwoo fwip
+5 | || argle bargle boo
+ | |'------------^ inner
+ | '---- outer
+ '"""));
+ });
+ });
+
+ group('a single-line span in a multiline span', () {
+ test('on the first line', () {
+ expect(
+ file
+ .span(17, 21)
+ .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+ equals("""
+ ,
+2 | / whiz bang boom
+ | | ^^^^ inner
+3 | | zip zap zop
+4 | | fwee fwoo fwip
+5 | | argle bargle boo
+ | '--- outer
+ '"""));
+ });
+
+ test('in the middle', () {
+ expect(
+ file
+ .span(31, 34)
+ .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+ equals("""
+ ,
+2 | / whiz bang boom
+3 | | zip zap zop
+ | | ^^^ inner
+4 | | fwee fwoo fwip
+5 | | argle bargle boo
+ | '--- outer
+ '"""));
+ });
+
+ test('on the last line', () {
+ expect(
+ file
+ .span(60, 66)
+ .highlightMultiple('inner', {file.span(12, 70): 'outer'}),
+ equals("""
+ ,
+2 | / whiz bang boom
+3 | | zip zap zop
+4 | | fwee fwoo fwip
+5 | | argle bargle boo
+ | | ^^^^^^ inner
+ | '--- outer
+ '"""));
+ });
+ });
+
+ group('writes headers when highlighting multiple files', () {
+ test('writes all file URLs', () {
+ final span2 = SourceFile.fromString('''
+quibble bibble boop
+''', url: 'file2.txt').span(8, 14);
+
+ expect(
+ file.span(31, 34).highlightMultiple('one', {span2: 'two'}), equals("""
+ ,--> file1.txt
+3 | zip zap zop
+ | ^^^ one
+ '
+ ,--> file2.txt
+1 | quibble bibble boop
+ | ====== two
+ '"""));
+ });
+
+ test('allows secondary spans to have null URL', () {
+ final span2 = SourceSpan(SourceLocation(1), SourceLocation(4), 'foo');
+
+ expect(
+ file.span(31, 34).highlightMultiple('one', {span2: 'two'}), equals("""
+ ,--> file1.txt
+3 | zip zap zop
+ | ^^^ one
+ '
+ ,
+1 | foo
+ | === two
+ '"""));
+ });
+
+ test('allows primary span to have null URL', () {
+ final span1 = SourceSpan(SourceLocation(1), SourceLocation(4), 'foo');
+
+ expect(
+ span1.highlightMultiple('one', {file.span(31, 34): 'two'}), equals("""
+ ,
+1 | foo
+ | ^^^ one
+ '
+ ,--> file1.txt
+3 | zip zap zop
+ | === two
+ '"""));
+ });
+ });
+
+ test('highlights multiple null URLs as separate files', () {
+ final span1 = SourceSpan(SourceLocation(1), SourceLocation(4), 'foo');
+ final span2 = SourceSpan(SourceLocation(1), SourceLocation(4), 'bar');
+
+ expect(span1.highlightMultiple('one', {span2: 'two'}), equals("""
+ ,
+1 | foo
+ | ^^^ one
+ '
+ ,
+1 | bar
+ | === two
+ '"""));
+ });
+
+ group('indents mutli-line labels', () {
+ test('for the primary label', () {
+ expect(file.span(17, 21).highlightMultiple('line 1\nline 2\nline 3', {}),
+ equals("""
+ ,
+2 | whiz bang boom
+ | ^^^^ line 1
+ | line 2
+ | line 3
+ '"""));
+ });
+
+ group('for a secondary label', () {
+ test('on the same line', () {
+ expect(
+ file.span(17, 21).highlightMultiple(
+ 'primary', {file.span(22, 26): 'line 1\nline 2\nline 3'}),
+ equals("""
+ ,
+2 | whiz bang boom
+ | ^^^^ primary
+ | ==== line 1
+ | line 2
+ | line 3
+ '"""));
+ });
+
+ test('on a different line', () {
+ expect(
+ file.span(17, 21).highlightMultiple(
+ 'primary', {file.span(31, 34): 'line 1\nline 2\nline 3'}),
+ equals("""
+ ,
+2 | whiz bang boom
+ | ^^^^ primary
+3 | zip zap zop
+ | === line 1
+ | line 2
+ | line 3
+ '"""));
+ });
+ });
+
+ group('for a multiline span', () {
+ test('that covers the whole last line', () {
+ expect(
+ file.span(12, 70).highlightMultiple('line 1\nline 2\nline 3', {}),
+ equals("""
+ ,
+2 | / whiz bang boom
+3 | | zip zap zop
+4 | | fwee fwoo fwip
+5 | | argle bargle boo
+ | '--- line 1
+ | line 2
+ | line 3
+ '"""));
+ });
+
+ test('that covers part of the last line', () {
+ expect(
+ file.span(12, 66).highlightMultiple('line 1\nline 2\nline 3', {}),
+ equals("""
+ ,
+2 | / whiz bang boom
+3 | | zip zap zop
+4 | | fwee fwoo fwip
+5 | | argle bargle boo
+ | '------------^ line 1
+ | line 2
+ | line 3
+ '"""));
+ });
+ });
+
+ test('with an overlapping span', () {
+ expect(
+ file.span(12, 70).highlightMultiple('line 1\nline 2\nline 3',
+ {file.span(54, 89): 'two', file.span(0, 27): 'three'}),
+ equals("""
+ ,
+1 | /- foo bar baz
+2 | |/ whiz bang boom
+ | '+--- three
+3 | | zip zap zop
+4 | | fwee fwoo fwip
+5 | /+ argle bargle boo
+ | |'--- line 1
+ | | line 2
+ | | line 3
+6 | | gibble bibble bop
+ | '---- two
+ '"""));
+ });
+ });
+}
diff --git a/pkgs/source_span/test/span_test.dart b/pkgs/source_span/test/span_test.dart
new file mode 100644
index 0000000..22c498e
--- /dev/null
+++ b/pkgs/source_span/test/span_test.dart
@@ -0,0 +1,432 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_span/source_span.dart';
+import 'package:source_span/src/colors.dart' as colors;
+import 'package:term_glyph/term_glyph.dart' as glyph;
+import 'package:test/test.dart';
+
+void main() {
+ late bool oldAscii;
+
+ setUpAll(() {
+ oldAscii = glyph.ascii;
+ glyph.ascii = true;
+ });
+
+ tearDownAll(() {
+ glyph.ascii = oldAscii;
+ });
+
+ late SourceSpan span;
+ setUp(() {
+ span = SourceSpan(SourceLocation(5, sourceUrl: 'foo.dart'),
+ SourceLocation(12, sourceUrl: 'foo.dart'), 'foo bar');
+ });
+
+ group('errors', () {
+ group('for new SourceSpan()', () {
+ test('source URLs must match', () {
+ final start = SourceLocation(0, sourceUrl: 'foo.dart');
+ final end = SourceLocation(1, sourceUrl: 'bar.dart');
+ expect(() => SourceSpan(start, end, '_'), throwsArgumentError);
+ });
+
+ test('end must come after start', () {
+ final start = SourceLocation(1);
+ final end = SourceLocation(0);
+ expect(() => SourceSpan(start, end, '_'), throwsArgumentError);
+ });
+
+ test('text must be the right length', () {
+ final start = SourceLocation(0);
+ final end = SourceLocation(1);
+ expect(() => SourceSpan(start, end, 'abc'), throwsArgumentError);
+ });
+ });
+
+ group('for new SourceSpanWithContext()', () {
+ test('context must contain text', () {
+ final start = SourceLocation(2);
+ final end = SourceLocation(5);
+ expect(() => SourceSpanWithContext(start, end, 'abc', '--axc--'),
+ throwsArgumentError);
+ });
+
+ test('text starts at start.column in context', () {
+ final start = SourceLocation(3);
+ final end = SourceLocation(5);
+ expect(() => SourceSpanWithContext(start, end, 'abc', '--abc--'),
+ throwsArgumentError);
+ });
+
+ test('text starts at start.column of line in multi-line context', () {
+ final start = SourceLocation(4, line: 55, column: 3);
+ final end = SourceLocation(7, line: 55, column: 6);
+ expect(() => SourceSpanWithContext(start, end, 'abc', '\n--abc--'),
+ throwsArgumentError);
+ expect(() => SourceSpanWithContext(start, end, 'abc', '\n----abc--'),
+ throwsArgumentError);
+ expect(() => SourceSpanWithContext(start, end, 'abc', '\n\n--abc--'),
+ throwsArgumentError);
+
+ // However, these are valid:
+ SourceSpanWithContext(start, end, 'abc', '\n---abc--');
+ SourceSpanWithContext(start, end, 'abc', '\n\n---abc--');
+ });
+
+ test('text can occur multiple times in context', () {
+ final start1 = SourceLocation(4, line: 55, column: 2);
+ final end1 = SourceLocation(7, line: 55, column: 5);
+ final start2 = SourceLocation(4, line: 55, column: 8);
+ final end2 = SourceLocation(7, line: 55, column: 11);
+ SourceSpanWithContext(start1, end1, 'abc', '--abc---abc--\n');
+ SourceSpanWithContext(start1, end1, 'abc', '--abc--abc--\n');
+ SourceSpanWithContext(start2, end2, 'abc', '--abc---abc--\n');
+ SourceSpanWithContext(start2, end2, 'abc', '---abc--abc--\n');
+ expect(
+ () => SourceSpanWithContext(start1, end1, 'abc', '---abc--abc--\n'),
+ throwsArgumentError);
+ expect(
+ () => SourceSpanWithContext(start2, end2, 'abc', '--abc--abc--\n'),
+ throwsArgumentError);
+ });
+ });
+
+ group('for union()', () {
+ test('source URLs must match', () {
+ final other = SourceSpan(SourceLocation(12, sourceUrl: 'bar.dart'),
+ SourceLocation(13, sourceUrl: 'bar.dart'), '_');
+
+ expect(() => span.union(other), throwsArgumentError);
+ });
+
+ test('spans may not be disjoint', () {
+ final other = SourceSpan(SourceLocation(13, sourceUrl: 'foo.dart'),
+ SourceLocation(14, sourceUrl: 'foo.dart'), '_');
+
+ expect(() => span.union(other), throwsArgumentError);
+ });
+ });
+
+ test('for compareTo() source URLs must match', () {
+ final other = SourceSpan(SourceLocation(12, sourceUrl: 'bar.dart'),
+ SourceLocation(13, sourceUrl: 'bar.dart'), '_');
+
+ expect(() => span.compareTo(other), throwsArgumentError);
+ });
+ });
+
+ test('fields work correctly', () {
+ expect(span.start, equals(SourceLocation(5, sourceUrl: 'foo.dart')));
+ expect(span.end, equals(SourceLocation(12, sourceUrl: 'foo.dart')));
+ expect(span.sourceUrl, equals(Uri.parse('foo.dart')));
+ expect(span.length, equals(7));
+ });
+
+ group('union()', () {
+ test('works with a preceding adjacent span', () {
+ final other = SourceSpan(SourceLocation(0, sourceUrl: 'foo.dart'),
+ SourceLocation(5, sourceUrl: 'foo.dart'), 'hey, ');
+
+ final result = span.union(other);
+ expect(result.start, equals(other.start));
+ expect(result.end, equals(span.end));
+ expect(result.text, equals('hey, foo bar'));
+ });
+
+ test('works with a preceding overlapping span', () {
+ final other = SourceSpan(SourceLocation(0, sourceUrl: 'foo.dart'),
+ SourceLocation(8, sourceUrl: 'foo.dart'), 'hey, foo');
+
+ final result = span.union(other);
+ expect(result.start, equals(other.start));
+ expect(result.end, equals(span.end));
+ expect(result.text, equals('hey, foo bar'));
+ });
+
+ test('works with a following adjacent span', () {
+ final other = SourceSpan(SourceLocation(12, sourceUrl: 'foo.dart'),
+ SourceLocation(16, sourceUrl: 'foo.dart'), ' baz');
+
+ final result = span.union(other);
+ expect(result.start, equals(span.start));
+ expect(result.end, equals(other.end));
+ expect(result.text, equals('foo bar baz'));
+ });
+
+ test('works with a following overlapping span', () {
+ final other = SourceSpan(SourceLocation(9, sourceUrl: 'foo.dart'),
+ SourceLocation(16, sourceUrl: 'foo.dart'), 'bar baz');
+
+ final result = span.union(other);
+ expect(result.start, equals(span.start));
+ expect(result.end, equals(other.end));
+ expect(result.text, equals('foo bar baz'));
+ });
+
+ test('works with an internal overlapping span', () {
+ final other = SourceSpan(SourceLocation(7, sourceUrl: 'foo.dart'),
+ SourceLocation(10, sourceUrl: 'foo.dart'), 'o b');
+
+ expect(span.union(other), equals(span));
+ });
+
+ test('works with an external overlapping span', () {
+ final other = SourceSpan(SourceLocation(0, sourceUrl: 'foo.dart'),
+ SourceLocation(16, sourceUrl: 'foo.dart'), 'hey, foo bar baz');
+
+ expect(span.union(other), equals(other));
+ });
+ });
+
+ group('subspan()', () {
+ group('errors', () {
+ test('start must be greater than zero', () {
+ expect(() => span.subspan(-1), throwsRangeError);
+ });
+
+ test('start must be less than or equal to length', () {
+ expect(() => span.subspan(span.length + 1), throwsRangeError);
+ });
+
+ test('end must be greater than start', () {
+ expect(() => span.subspan(2, 1), throwsRangeError);
+ });
+
+ test('end must be less than or equal to length', () {
+ expect(() => span.subspan(0, span.length + 1), throwsRangeError);
+ });
+ });
+
+ test('preserves the source URL', () {
+ final result = span.subspan(1, 2);
+ expect(result.start.sourceUrl, equals(span.sourceUrl));
+ expect(result.end.sourceUrl, equals(span.sourceUrl));
+ });
+
+ test('preserves the context', () {
+ final start = SourceLocation(2);
+ final end = SourceLocation(5);
+ final span = SourceSpanWithContext(start, end, 'abc', '--abc--');
+ expect(span.subspan(1, 2).context, equals('--abc--'));
+ });
+
+ group('returns the original span', () {
+ test('with an implicit end', () => expect(span.subspan(0), equals(span)));
+
+ test('with an explicit end',
+ () => expect(span.subspan(0, span.length), equals(span)));
+ });
+
+ group('within a single line', () {
+ test('returns a strict substring of the original span', () {
+ final result = span.subspan(1, 5);
+ expect(result.text, equals('oo b'));
+ expect(result.start.offset, equals(6));
+ expect(result.start.line, equals(0));
+ expect(result.start.column, equals(6));
+ expect(result.end.offset, equals(10));
+ expect(result.end.line, equals(0));
+ expect(result.end.column, equals(10));
+ });
+
+ test('an implicit end goes to the end of the original span', () {
+ final result = span.subspan(1);
+ expect(result.text, equals('oo bar'));
+ expect(result.start.offset, equals(6));
+ expect(result.start.line, equals(0));
+ expect(result.start.column, equals(6));
+ expect(result.end.offset, equals(12));
+ expect(result.end.line, equals(0));
+ expect(result.end.column, equals(12));
+ });
+
+ test('can return an empty span', () {
+ final result = span.subspan(3, 3);
+ expect(result.text, isEmpty);
+ expect(result.start.offset, equals(8));
+ expect(result.start.line, equals(0));
+ expect(result.start.column, equals(8));
+ expect(result.end, equals(result.start));
+ });
+ });
+
+ group('across multiple lines', () {
+ setUp(() {
+ span = SourceSpan(
+ SourceLocation(5, line: 2, column: 0),
+ SourceLocation(16, line: 4, column: 3),
+ 'foo\n'
+ 'bar\n'
+ 'baz');
+ });
+
+ test('with start and end in the middle of a line', () {
+ final result = span.subspan(2, 5);
+ expect(result.text, equals('o\nb'));
+ expect(result.start.offset, equals(7));
+ expect(result.start.line, equals(2));
+ expect(result.start.column, equals(2));
+ expect(result.end.offset, equals(10));
+ expect(result.end.line, equals(3));
+ expect(result.end.column, equals(1));
+ });
+
+ test('with start at the end of a line', () {
+ final result = span.subspan(3, 5);
+ expect(result.text, equals('\nb'));
+ expect(result.start.offset, equals(8));
+ expect(result.start.line, equals(2));
+ expect(result.start.column, equals(3));
+ });
+
+ test('with start at the beginning of a line', () {
+ final result = span.subspan(4, 5);
+ expect(result.text, equals('b'));
+ expect(result.start.offset, equals(9));
+ expect(result.start.line, equals(3));
+ expect(result.start.column, equals(0));
+ });
+
+ test('with end at the end of a line', () {
+ final result = span.subspan(2, 3);
+ expect(result.text, equals('o'));
+ expect(result.end.offset, equals(8));
+ expect(result.end.line, equals(2));
+ expect(result.end.column, equals(3));
+ });
+
+ test('with end at the beginning of a line', () {
+ final result = span.subspan(2, 4);
+ expect(result.text, equals('o\n'));
+ expect(result.end.offset, equals(9));
+ expect(result.end.line, equals(3));
+ expect(result.end.column, equals(0));
+ });
+ });
+ });
+
+ group('message()', () {
+ test('prints the text being described', () {
+ expect(span.message('oh no'), equals("""
+line 1, column 6 of foo.dart: oh no
+ ,
+1 | foo bar
+ | ^^^^^^^
+ '"""));
+ });
+
+ test('gracefully handles a missing source URL', () {
+ final span = SourceSpan(SourceLocation(5), SourceLocation(12), 'foo bar');
+
+ expect(span.message('oh no'), equalsIgnoringWhitespace("""
+line 1, column 6: oh no
+ ,
+1 | foo bar
+ | ^^^^^^^
+ '"""));
+ });
+
+ test('gracefully handles empty text', () {
+ final span = SourceSpan(SourceLocation(5), SourceLocation(5), '');
+
+ expect(span.message('oh no'), equals('line 1, column 6: oh no'));
+ });
+
+ test("doesn't colorize if color is false", () {
+ expect(span.message('oh no', color: false), equals("""
+line 1, column 6 of foo.dart: oh no
+ ,
+1 | foo bar
+ | ^^^^^^^
+ '"""));
+ });
+
+ test('colorizes if color is true', () {
+ expect(span.message('oh no', color: true), equals("""
+line 1, column 6 of foo.dart: oh no
+${colors.blue} ,${colors.none}
+${colors.blue}1 |${colors.none} ${colors.red}foo bar${colors.none}
+${colors.blue} |${colors.none} ${colors.red}^^^^^^^${colors.none}
+${colors.blue} '${colors.none}"""));
+ });
+
+ test("uses the given color if it's passed", () {
+ expect(span.message('oh no', color: colors.yellow), equals("""
+line 1, column 6 of foo.dart: oh no
+${colors.blue} ,${colors.none}
+${colors.blue}1 |${colors.none} ${colors.yellow}foo bar${colors.none}
+${colors.blue} |${colors.none} ${colors.yellow}^^^^^^^${colors.none}
+${colors.blue} '${colors.none}"""));
+ });
+
+ test('with context, underlines the right column', () {
+ final spanWithContext = SourceSpanWithContext(
+ SourceLocation(5, sourceUrl: 'foo.dart'),
+ SourceLocation(12, sourceUrl: 'foo.dart'),
+ 'foo bar',
+ '-----foo bar-----');
+
+ expect(spanWithContext.message('oh no', color: colors.yellow), equals("""
+line 1, column 6 of foo.dart: oh no
+${colors.blue} ,${colors.none}
+${colors.blue}1 |${colors.none} -----${colors.yellow}foo bar${colors.none}-----
+${colors.blue} |${colors.none} ${colors.yellow} ^^^^^^^${colors.none}
+${colors.blue} '${colors.none}"""));
+ });
+ });
+
+ group('compareTo()', () {
+ test('sorts by start location first', () {
+ final other = SourceSpan(SourceLocation(6, sourceUrl: 'foo.dart'),
+ SourceLocation(14, sourceUrl: 'foo.dart'), 'oo bar b');
+
+ expect(span.compareTo(other), lessThan(0));
+ expect(other.compareTo(span), greaterThan(0));
+ });
+
+ test('sorts by length second', () {
+ final other = SourceSpan(SourceLocation(5, sourceUrl: 'foo.dart'),
+ SourceLocation(14, sourceUrl: 'foo.dart'), 'foo bar b');
+
+ expect(span.compareTo(other), lessThan(0));
+ expect(other.compareTo(span), greaterThan(0));
+ });
+
+ test('considers equal spans equal', () {
+ expect(span.compareTo(span), equals(0));
+ });
+ });
+
+ group('equality', () {
+ test('two spans with the same locations are equal', () {
+ final other = SourceSpan(SourceLocation(5, sourceUrl: 'foo.dart'),
+ SourceLocation(12, sourceUrl: 'foo.dart'), 'foo bar');
+
+ expect(span, equals(other));
+ });
+
+ test("a different start isn't equal", () {
+ final other = SourceSpan(SourceLocation(0, sourceUrl: 'foo.dart'),
+ SourceLocation(12, sourceUrl: 'foo.dart'), 'hey, foo bar');
+
+ expect(span, isNot(equals(other)));
+ });
+
+ test("a different end isn't equal", () {
+ final other = SourceSpan(SourceLocation(5, sourceUrl: 'foo.dart'),
+ SourceLocation(16, sourceUrl: 'foo.dart'), 'foo bar baz');
+
+ expect(span, isNot(equals(other)));
+ });
+
+ test("a different source URL isn't equal", () {
+ final other = SourceSpan(SourceLocation(5, sourceUrl: 'bar.dart'),
+ SourceLocation(12, sourceUrl: 'bar.dart'), 'foo bar');
+
+ expect(span, isNot(equals(other)));
+ });
+ });
+}
diff --git a/pkgs/source_span/test/utils_test.dart b/pkgs/source_span/test/utils_test.dart
new file mode 100644
index 0000000..91397c0
--- /dev/null
+++ b/pkgs/source_span/test/utils_test.dart
@@ -0,0 +1,58 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_span/src/utils.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('find line start', () {
+ test('skip entries in wrong column', () {
+ const context = '0_bb\n1_bbb\n2b____\n3bbb\n';
+ final index = findLineStart(context, 'b', 1)!;
+ expect(index, 11);
+ expect(context.substring(index - 1, index + 3), '\n2b_');
+ });
+
+ test('end of line column for empty text', () {
+ const context = '0123\n56789\nabcdefgh\n';
+ final index = findLineStart(context, '', 5)!;
+ expect(index, 5);
+ expect(context[index], '5');
+ });
+
+ test('column at the end of the file for empty text', () {
+ var context = '0\n2\n45\n';
+ var index = findLineStart(context, '', 2)!;
+ expect(index, 4);
+ expect(context[index], '4');
+
+ context = '0\n2\n45';
+ index = findLineStart(context, '', 2)!;
+ expect(index, 4);
+ });
+
+ test('empty text in empty context', () {
+ final index = findLineStart('', '', 0);
+ expect(index, 0);
+ });
+
+ test('found on the first line', () {
+ const context = '0\n2\n45\n';
+ final index = findLineStart(context, '0', 0);
+ expect(index, 0);
+ });
+
+ test('finds text that starts with a newline', () {
+ const context = '0\n2\n45\n';
+ final index = findLineStart(context, '\n2', 1);
+ expect(index, 0);
+ });
+
+ test('not found', () {
+ const context = '0\n2\n45\n';
+ final index = findLineStart(context, '0', 1);
+ expect(index, isNull);
+ });
+ });
+}