Merge package:pubspec_parse into the tools monorepo
diff --git a/pkgs/pubspec_parse/.github/dependabot.yml b/pkgs/pubspec_parse/.github/dependabot.yml
new file mode 100644
index 0000000..cde02ad
--- /dev/null
+++ b/pkgs/pubspec_parse/.github/dependabot.yml
@@ -0,0 +1,15 @@
+# Dependabot configuration file.
+# See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates
+version: 2
+
+updates:
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: monthly
+ labels:
+ - autosubmit
+ groups:
+ github-actions:
+ patterns:
+ - "*"
diff --git a/pkgs/pubspec_parse/.github/workflows/publish.yaml b/pkgs/pubspec_parse/.github/workflows/publish.yaml
new file mode 100644
index 0000000..27157a0
--- /dev/null
+++ b/pkgs/pubspec_parse/.github/workflows/publish.yaml
@@ -0,0 +1,17 @@
+# A CI configuration to auto-publish pub packages.
+
+name: Publish
+
+on:
+ pull_request:
+ branches: [ master ]
+ push:
+ tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ]
+
+jobs:
+ publish:
+ if: ${{ github.repository_owner == 'dart-lang' }}
+ uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main
+ permissions:
+ id-token: write # Required for authentication using OIDC
+ pull-requests: write # Required for writing the pull request note
diff --git a/pkgs/pubspec_parse/.github/workflows/test-package.yml b/pkgs/pubspec_parse/.github/workflows/test-package.yml
new file mode 100644
index 0000000..922d6c2
--- /dev/null
+++ b/pkgs/pubspec_parse/.github/workflows/test-package.yml
@@ -0,0 +1,60 @@
+name: Dart CI
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+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]
+ sdk: [3.2, 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 --run-skipped
+ if: always() && steps.install.outcome == 'success'
diff --git a/pkgs/pubspec_parse/.gitignore b/pkgs/pubspec_parse/.gitignore
new file mode 100644
index 0000000..ec8eae3
--- /dev/null
+++ b/pkgs/pubspec_parse/.gitignore
@@ -0,0 +1,4 @@
+# Don’t commit the following directories created by pub.
+.dart_tool/
+.packages
+pubspec.lock
diff --git a/pkgs/pubspec_parse/CHANGELOG.md b/pkgs/pubspec_parse/CHANGELOG.md
new file mode 100644
index 0000000..251d4cc
--- /dev/null
+++ b/pkgs/pubspec_parse/CHANGELOG.md
@@ -0,0 +1,103 @@
+## 1.4.0-wip
+
+- Require Dart 3.2
+- Seal the `Dependency` class.
+- Set `Pubspec.environment` to non-nullable.
+- Remove deprecated package_api_docs rule
+
+## 1.3.0
+
+- Require Dart 3.0
+- Added support for `ignored_advisories` field.
+- Added structural equality for `Dependency` subclasses and `HostedDetails`.
+
+## 1.2.3
+
+- Added topics to `pubspec.yaml`.
+
+## 1.2.2
+
+- Require Dart SDK >= 2.18.0
+- Required `json_annotation: ^4.8.0`
+- Added support for `topics` field.
+
+## 1.2.1
+
+- Added support for `funding` field.
+
+## 1.2.0
+
+- Added support for `screenshots` field.
+- Update `HostedDetails` to reflect how `hosted` dependencies are parsed in
+ Dart 2.15:
+ - Add `HostedDetails.declaredName` as the (optional) `name` property in a
+ `hosted` block.
+ - `HostedDetails.name` now falls back to the name of the dependency if no
+ name is declared in the block.
+- Require Dart SDK >= 2.14.0
+
+## 1.1.0
+
+- Export `HostedDetails` publicly.
+
+## 1.0.0
+
+- Migrate to null-safety.
+- Pubspec: `author` and `authors` are both now deprecated.
+ See https://dart.dev/tools/pub/pubspec#authorauthors
+
+## 0.1.8
+
+- Allow the latest `package:pub_semver`.
+
+## 0.1.7
+
+- Allow `package:yaml` `v3.x`.
+
+## 0.1.6
+
+- Update SDK requirement to `>=2.7.0 <3.0.0`.
+- Allow `package:json_annotation` `v4.x`.
+
+## 0.1.5
+
+- Update SDK requirement to `>=2.2.0 <3.0.0`.
+- Support the latest `package:json_annotation`.
+
+## 0.1.4
+
+- Added `lenient` named argument to `Pubspec.fromJson` to ignore format and type errors.
+
+## 0.1.3
+
+- Added support for `flutter`, `issue_tracker`, `publish_to`, and `repository`
+ fields.
+
+## 0.1.2+3
+
+- Support the latest version of `package:json_annotation`.
+
+## 0.1.2+2
+
+- Support `package:json_annotation` v1.
+
+## 0.1.2+1
+
+- Support the Dart 2 stable release.
+
+## 0.1.2
+
+- Allow superfluous `version` keys with `git` and `path` dependencies.
+- Improve errors when unsupported keys are provided in dependencies.
+- Provide better errors with invalid `sdk` dependency values.
+- Support "scp-like syntax" for Git SSH URIs in the form
+ `[user@]host.xz:path/to/repo.git/`.
+
+## 0.1.1
+
+- Fixed name collision with error type in latest `package:json_annotation`.
+- Improved parsing of hosted dependencies and environment constraints.
+
+## 0.1.0
+
+- Initial release.
diff --git a/pkgs/pubspec_parse/LICENSE b/pkgs/pubspec_parse/LICENSE
new file mode 100644
index 0000000..4d1ad40
--- /dev/null
+++ b/pkgs/pubspec_parse/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2018, 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/pubspec_parse/README.md b/pkgs/pubspec_parse/README.md
new file mode 100644
index 0000000..916742a
--- /dev/null
+++ b/pkgs/pubspec_parse/README.md
@@ -0,0 +1,12 @@
+[](https://github.com/dart-lang/pubspec_parse/actions/workflows/test-package.yml)
+[](https://pub.dev/packages/pubspec_parse)
+[](https://pub.dev/packages/pubspec_parse/publisher)
+
+## What's this?
+
+Supports parsing `pubspec.yaml` files with robust error reporting and support
+for most of the documented features.
+
+## More information
+
+Read more about the [pubspec format](https://dart.dev/tools/pub/pubspec).
diff --git a/pkgs/pubspec_parse/analysis_options.yaml b/pkgs/pubspec_parse/analysis_options.yaml
new file mode 100644
index 0000000..93eeebf
--- /dev/null
+++ b/pkgs/pubspec_parse/analysis_options.yaml
@@ -0,0 +1,30 @@
+# https://dart.dev/guides/language/analysis-options
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
+ strict-inference: 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
+ - prefer_final_locals
+ - require_trailing_commas
+ - unnecessary_await_in_return
+ - use_string_buffers
diff --git a/pkgs/pubspec_parse/build.yaml b/pkgs/pubspec_parse/build.yaml
new file mode 100644
index 0000000..2003bc2
--- /dev/null
+++ b/pkgs/pubspec_parse/build.yaml
@@ -0,0 +1,25 @@
+# Read about `build.yaml` at https://pub.dev/packages/build_config
+# To update generated code, run `pub run build_runner build`
+targets:
+ $default:
+ builders:
+ json_serializable:
+ generate_for:
+ - lib/src/pubspec.dart
+ - lib/src/dependency.dart
+ options:
+ any_map: true
+ checked: true
+ create_to_json: false
+ field_rename: snake
+
+ # The end-user of a builder which applies "source_gen|combining_builder"
+ # may configure the builder to ignore specific lints for their project
+ source_gen|combining_builder:
+ options:
+ ignore_for_file:
+ - deprecated_member_use_from_same_package
+ - lines_longer_than_80_chars
+ - require_trailing_commas
+ # https://github.com/google/json_serializable.dart/issues/945
+ - unnecessary_cast
diff --git a/pkgs/pubspec_parse/dart_test.yaml b/pkgs/pubspec_parse/dart_test.yaml
new file mode 100644
index 0000000..1d7ac69
--- /dev/null
+++ b/pkgs/pubspec_parse/dart_test.yaml
@@ -0,0 +1,3 @@
+tags:
+ presubmit-only:
+ skip: "Should only be run during presubmit"
diff --git a/pkgs/pubspec_parse/lib/pubspec_parse.dart b/pkgs/pubspec_parse/lib/pubspec_parse.dart
new file mode 100644
index 0000000..b5c12e4
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/pubspec_parse.dart
@@ -0,0 +1,14 @@
+// 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.
+
+export 'src/dependency.dart'
+ show
+ Dependency,
+ GitDependency,
+ HostedDependency,
+ HostedDetails,
+ PathDependency,
+ SdkDependency;
+export 'src/pubspec.dart' show Pubspec;
+export 'src/screenshot.dart' show Screenshot;
diff --git a/pkgs/pubspec_parse/lib/src/dependency.dart b/pkgs/pubspec_parse/lib/src/dependency.dart
new file mode 100644
index 0000000..24c65ea
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/dependency.dart
@@ -0,0 +1,277 @@
+// 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 'package:collection/collection.dart';
+import 'package:json_annotation/json_annotation.dart';
+import 'package:pub_semver/pub_semver.dart';
+import 'package:yaml/yaml.dart';
+
+part 'dependency.g.dart';
+
+Map<String, Dependency> parseDeps(Map? source) =>
+ source?.map((k, v) {
+ final key = k as String;
+ Dependency? value;
+ try {
+ value = _fromJson(v, k);
+ } on CheckedFromJsonException catch (e) {
+ if (e.map is! YamlMap) {
+ // This is likely a "synthetic" map created from a String value
+ // Use `source` to throw this exception with an actual YamlMap and
+ // extract the associated error information.
+ throw CheckedFromJsonException(source, key, e.className!, e.message);
+ }
+ rethrow;
+ }
+
+ if (value == null) {
+ throw CheckedFromJsonException(
+ source,
+ key,
+ 'Pubspec',
+ 'Not a valid dependency value.',
+ );
+ }
+ return MapEntry(key, value);
+ }) ??
+ {};
+
+const _sourceKeys = ['sdk', 'git', 'path', 'hosted'];
+
+/// Returns `null` if the data could not be parsed.
+Dependency? _fromJson(Object? data, String name) {
+ if (data is String || data == null) {
+ return _$HostedDependencyFromJson({'version': data});
+ }
+
+ if (data is Map) {
+ final matchedKeys =
+ data.keys.cast<String>().where((key) => key != 'version').toList();
+
+ if (data.isEmpty || (matchedKeys.isEmpty && data.containsKey('version'))) {
+ return _$HostedDependencyFromJson(data);
+ } else {
+ final firstUnrecognizedKey =
+ matchedKeys.firstWhereOrNull((k) => !_sourceKeys.contains(k));
+
+ return $checkedNew<Dependency>('Dependency', data, () {
+ if (firstUnrecognizedKey != null) {
+ throw UnrecognizedKeysException(
+ [firstUnrecognizedKey],
+ data,
+ _sourceKeys,
+ );
+ }
+ if (matchedKeys.length > 1) {
+ throw CheckedFromJsonException(
+ data,
+ matchedKeys[1],
+ 'Dependency',
+ 'A dependency may only have one source.',
+ );
+ }
+
+ final key = matchedKeys.single;
+
+ return switch (key) {
+ 'git' => GitDependency.fromData(data[key]),
+ 'path' => PathDependency.fromData(data[key]),
+ 'sdk' => _$SdkDependencyFromJson(data),
+ 'hosted' => _$HostedDependencyFromJson(data)
+ ..hosted?._nameOfPackage = name,
+ _ => throw StateError('There is a bug in pubspec_parse.'),
+ };
+ });
+ }
+ }
+
+ // Not a String or a Map – return null so parent logic can throw proper error
+ return null;
+}
+
+sealed class Dependency {}
+
+@JsonSerializable()
+class SdkDependency extends Dependency {
+ final String sdk;
+ @JsonKey(fromJson: _constraintFromString)
+ final VersionConstraint version;
+
+ SdkDependency(this.sdk, {VersionConstraint? version})
+ : version = version ?? VersionConstraint.any;
+
+ @override
+ bool operator ==(Object other) =>
+ other is SdkDependency && other.sdk == sdk && other.version == version;
+
+ @override
+ int get hashCode => Object.hash(sdk, version);
+
+ @override
+ String toString() => 'SdkDependency: $sdk';
+}
+
+@JsonSerializable()
+class GitDependency extends Dependency {
+ @JsonKey(fromJson: parseGitUri)
+ final Uri url;
+ final String? ref;
+ final String? path;
+
+ GitDependency(this.url, {this.ref, this.path});
+
+ factory GitDependency.fromData(Object? data) {
+ if (data is String) {
+ data = {'url': data};
+ }
+
+ if (data is Map) {
+ return _$GitDependencyFromJson(data);
+ }
+
+ throw ArgumentError.value(data, 'git', 'Must be a String or a Map.');
+ }
+
+ @override
+ bool operator ==(Object other) =>
+ other is GitDependency &&
+ other.url == url &&
+ other.ref == ref &&
+ other.path == path;
+
+ @override
+ int get hashCode => Object.hash(url, ref, path);
+
+ @override
+ String toString() => 'GitDependency: url@$url';
+}
+
+Uri? parseGitUriOrNull(String? value) =>
+ value == null ? null : parseGitUri(value);
+
+Uri parseGitUri(String value) => _tryParseScpUri(value) ?? Uri.parse(value);
+
+/// Supports URIs like `[user@]host.xz:path/to/repo.git/`
+/// See https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a
+Uri? _tryParseScpUri(String value) {
+ final colonIndex = value.indexOf(':');
+
+ if (colonIndex < 0) {
+ return null;
+ } else if (colonIndex == value.indexOf('://')) {
+ // If the first colon is part of a scheme, it's not an scp-like URI
+ return null;
+ }
+ final slashIndex = value.indexOf('/');
+
+ if (slashIndex >= 0 && slashIndex < colonIndex) {
+ // Per docs: This syntax is only recognized if there are no slashes before
+ // the first colon. This helps differentiate a local path that contains a
+ // colon. For example the local path foo:bar could be specified as an
+ // absolute path or ./foo:bar to avoid being misinterpreted as an ssh url.
+ return null;
+ }
+
+ final atIndex = value.indexOf('@');
+ if (colonIndex > atIndex) {
+ final user = atIndex >= 0 ? value.substring(0, atIndex) : null;
+ final host = value.substring(atIndex + 1, colonIndex);
+ final path = value.substring(colonIndex + 1);
+ return Uri(scheme: 'ssh', userInfo: user, host: host, path: path);
+ }
+ return null;
+}
+
+class PathDependency extends Dependency {
+ final String path;
+
+ PathDependency(this.path);
+
+ factory PathDependency.fromData(Object? data) {
+ if (data is String) {
+ return PathDependency(data);
+ }
+ throw ArgumentError.value(data, 'path', 'Must be a String.');
+ }
+
+ @override
+ bool operator ==(Object other) =>
+ other is PathDependency && other.path == path;
+
+ @override
+ int get hashCode => path.hashCode;
+
+ @override
+ String toString() => 'PathDependency: path@$path';
+}
+
+@JsonSerializable(disallowUnrecognizedKeys: true)
+class HostedDependency extends Dependency {
+ @JsonKey(fromJson: _constraintFromString)
+ final VersionConstraint version;
+
+ @JsonKey(disallowNullValue: true)
+ final HostedDetails? hosted;
+
+ HostedDependency({VersionConstraint? version, this.hosted})
+ : version = version ?? VersionConstraint.any;
+
+ @override
+ bool operator ==(Object other) =>
+ other is HostedDependency &&
+ other.version == version &&
+ other.hosted == hosted;
+
+ @override
+ int get hashCode => Object.hash(version, hosted);
+
+ @override
+ String toString() => 'HostedDependency: $version';
+}
+
+@JsonSerializable(disallowUnrecognizedKeys: true)
+class HostedDetails {
+ /// The name of the target dependency as declared in a `hosted` block.
+ ///
+ /// This may be null if no explicit name is present, for instance because the
+ /// hosted dependency was declared as a string (`hosted: pub.example.org`).
+ @JsonKey(name: 'name')
+ final String? declaredName;
+
+ @JsonKey(fromJson: parseGitUriOrNull, disallowNullValue: true)
+ final Uri? url;
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ String? _nameOfPackage;
+
+ /// The name of this package on the package repository.
+ ///
+ /// If this hosted block has a [declaredName], that one will be used.
+ /// Otherwise, the name will be inferred from the surrounding package name.
+ String get name => declaredName ?? _nameOfPackage!;
+
+ HostedDetails(this.declaredName, this.url);
+
+ factory HostedDetails.fromJson(Object data) {
+ if (data is String) {
+ data = {'url': data};
+ }
+
+ if (data is Map) {
+ return _$HostedDetailsFromJson(data);
+ }
+
+ throw ArgumentError.value(data, 'hosted', 'Must be a Map or String.');
+ }
+
+ @override
+ bool operator ==(Object other) =>
+ other is HostedDetails && other.name == name && other.url == url;
+
+ @override
+ int get hashCode => Object.hash(name, url);
+}
+
+VersionConstraint _constraintFromString(String? input) =>
+ input == null ? VersionConstraint.any : VersionConstraint.parse(input);
diff --git a/pkgs/pubspec_parse/lib/src/dependency.g.dart b/pkgs/pubspec_parse/lib/src/dependency.g.dart
new file mode 100644
index 0000000..1a504f1
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/dependency.g.dart
@@ -0,0 +1,72 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+// ignore_for_file: deprecated_member_use_from_same_package, lines_longer_than_80_chars, require_trailing_commas, unnecessary_cast
+
+part of 'dependency.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+SdkDependency _$SdkDependencyFromJson(Map json) => $checkedCreate(
+ 'SdkDependency',
+ json,
+ ($checkedConvert) {
+ final val = SdkDependency(
+ $checkedConvert('sdk', (v) => v as String),
+ version: $checkedConvert(
+ 'version', (v) => _constraintFromString(v as String?)),
+ );
+ return val;
+ },
+ );
+
+GitDependency _$GitDependencyFromJson(Map json) => $checkedCreate(
+ 'GitDependency',
+ json,
+ ($checkedConvert) {
+ final val = GitDependency(
+ $checkedConvert('url', (v) => parseGitUri(v as String)),
+ ref: $checkedConvert('ref', (v) => v as String?),
+ path: $checkedConvert('path', (v) => v as String?),
+ );
+ return val;
+ },
+ );
+
+HostedDependency _$HostedDependencyFromJson(Map json) => $checkedCreate(
+ 'HostedDependency',
+ json,
+ ($checkedConvert) {
+ $checkKeys(
+ json,
+ allowedKeys: const ['version', 'hosted'],
+ disallowNullValues: const ['hosted'],
+ );
+ final val = HostedDependency(
+ version: $checkedConvert(
+ 'version', (v) => _constraintFromString(v as String?)),
+ hosted: $checkedConvert('hosted',
+ (v) => v == null ? null : HostedDetails.fromJson(v as Object)),
+ );
+ return val;
+ },
+ );
+
+HostedDetails _$HostedDetailsFromJson(Map json) => $checkedCreate(
+ 'HostedDetails',
+ json,
+ ($checkedConvert) {
+ $checkKeys(
+ json,
+ allowedKeys: const ['name', 'url'],
+ disallowNullValues: const ['url'],
+ );
+ final val = HostedDetails(
+ $checkedConvert('name', (v) => v as String?),
+ $checkedConvert('url', (v) => parseGitUriOrNull(v as String?)),
+ );
+ return val;
+ },
+ fieldKeyMap: const {'declaredName': 'name'},
+ );
diff --git a/pkgs/pubspec_parse/lib/src/pubspec.dart b/pkgs/pubspec_parse/lib/src/pubspec.dart
new file mode 100644
index 0000000..1317a23
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/pubspec.dart
@@ -0,0 +1,226 @@
+// 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 'package:checked_yaml/checked_yaml.dart';
+import 'package:json_annotation/json_annotation.dart';
+import 'package:pub_semver/pub_semver.dart';
+
+import 'dependency.dart';
+import 'screenshot.dart';
+
+part 'pubspec.g.dart';
+
+@JsonSerializable()
+class Pubspec {
+ // TODO: executables
+
+ final String name;
+
+ @JsonKey(fromJson: _versionFromString)
+ final Version? version;
+
+ final String? description;
+
+ /// This should be a URL pointing to the website for the package.
+ final String? homepage;
+
+ /// Specifies where to publish this package.
+ ///
+ /// Accepted values: `null`, `'none'` or an `http` or `https` URL.
+ ///
+ /// [More information](https://dart.dev/tools/pub/pubspec#publish_to).
+ final String? publishTo;
+
+ /// Optional field to specify the source code repository of the package.
+ /// Useful when a package has both a home page and a repository.
+ final Uri? repository;
+
+ /// Optional field to a web page where developers can report new issues or
+ /// view existing ones.
+ final Uri? issueTracker;
+
+ /// Optional field to list the URLs where the package authors accept
+ /// support or funding.
+ final List<Uri>? funding;
+
+ /// Optional field to list the topics that this packages belongs to.
+ final List<String>? topics;
+
+ /// Optional field to list advisories to be ignored by the client.
+ final List<String>? ignoredAdvisories;
+
+ /// Optional field for specifying included screenshot files.
+ @JsonKey(fromJson: parseScreenshots)
+ final List<Screenshot>? screenshots;
+
+ /// If there is exactly 1 value in [authors], returns it.
+ ///
+ /// If there are 0 or more than 1, returns `null`.
+ @Deprecated(
+ 'See https://dart.dev/tools/pub/pubspec#authorauthors',
+ )
+ String? get author {
+ if (authors.length == 1) {
+ return authors.single;
+ }
+ return null;
+ }
+
+ @Deprecated(
+ 'See https://dart.dev/tools/pub/pubspec#authorauthors',
+ )
+ final List<String> authors;
+ final String? documentation;
+
+ @JsonKey(fromJson: _environmentMap)
+ final Map<String, VersionConstraint?> environment;
+
+ @JsonKey(fromJson: parseDeps)
+ final Map<String, Dependency> dependencies;
+
+ @JsonKey(fromJson: parseDeps)
+ final Map<String, Dependency> devDependencies;
+
+ @JsonKey(fromJson: parseDeps)
+ final Map<String, Dependency> dependencyOverrides;
+
+ /// Optional configuration specific to [Flutter](https://flutter.io/)
+ /// packages.
+ ///
+ /// May include
+ /// [assets](https://flutter.io/docs/development/ui/assets-and-images)
+ /// and other settings.
+ final Map<String, dynamic>? flutter;
+
+ /// If [author] and [authors] are both provided, their values are combined
+ /// with duplicates eliminated.
+ Pubspec(
+ this.name, {
+ this.version,
+ this.publishTo,
+ @Deprecated(
+ 'See https://dart.dev/tools/pub/pubspec#authorauthors',
+ )
+ String? author,
+ @Deprecated(
+ 'See https://dart.dev/tools/pub/pubspec#authorauthors',
+ )
+ List<String>? authors,
+ Map<String, VersionConstraint?>? environment,
+ this.homepage,
+ this.repository,
+ this.issueTracker,
+ this.funding,
+ this.topics,
+ this.ignoredAdvisories,
+ this.screenshots,
+ this.documentation,
+ this.description,
+ Map<String, Dependency>? dependencies,
+ Map<String, Dependency>? devDependencies,
+ Map<String, Dependency>? dependencyOverrides,
+ this.flutter,
+ }) :
+ // ignore: deprecated_member_use_from_same_package
+ authors = _normalizeAuthors(author, authors),
+ environment = environment ?? const {},
+ dependencies = dependencies ?? const {},
+ devDependencies = devDependencies ?? const {},
+ dependencyOverrides = dependencyOverrides ?? const {} {
+ if (name.isEmpty) {
+ throw ArgumentError.value(name, 'name', '"name" cannot be empty.');
+ }
+
+ if (publishTo != null && publishTo != 'none') {
+ try {
+ final targetUri = Uri.parse(publishTo!);
+ if (!(targetUri.isScheme('http') || targetUri.isScheme('https'))) {
+ throw const FormatException('Must be an http or https URL.');
+ }
+ } on FormatException catch (e) {
+ throw ArgumentError.value(publishTo, 'publishTo', e.message);
+ }
+ }
+ }
+
+ factory Pubspec.fromJson(Map json, {bool lenient = false}) {
+ if (lenient) {
+ while (json.isNotEmpty) {
+ // Attempting to remove top-level properties that cause parsing errors.
+ try {
+ return _$PubspecFromJson(json);
+ } on CheckedFromJsonException catch (e) {
+ if (e.map == json && json.containsKey(e.key)) {
+ json = Map.from(json)..remove(e.key);
+ continue;
+ }
+ rethrow;
+ }
+ }
+ }
+
+ return _$PubspecFromJson(json);
+ }
+
+ /// Parses source [yaml] into [Pubspec].
+ ///
+ /// When [lenient] is set, top-level property-parsing or type cast errors are
+ /// ignored and `null` values are returned.
+ factory Pubspec.parse(String yaml, {Uri? sourceUrl, bool lenient = false}) =>
+ checkedYamlDecode(
+ yaml,
+ (map) => Pubspec.fromJson(map!, lenient: lenient),
+ sourceUrl: sourceUrl,
+ );
+
+ static List<String> _normalizeAuthors(String? author, List<String>? authors) {
+ final value = <String>{
+ if (author != null) author,
+ ...?authors,
+ };
+ return value.toList();
+ }
+}
+
+Version? _versionFromString(String? input) =>
+ input == null ? null : Version.parse(input);
+
+Map<String, VersionConstraint?> _environmentMap(Map? source) =>
+ source?.map((k, value) {
+ final key = k as String;
+ if (key == 'dart') {
+ // github.com/dart-lang/pub/blob/d84173eeb03c3/lib/src/pubspec.dart#L342
+ // 'dart' is not allowed as a key!
+ throw CheckedFromJsonException(
+ source,
+ 'dart',
+ 'VersionConstraint',
+ 'Use "sdk" to for Dart SDK constraints.',
+ badKey: true,
+ );
+ }
+
+ VersionConstraint? constraint;
+ if (value == null) {
+ constraint = null;
+ } else if (value is String) {
+ try {
+ constraint = VersionConstraint.parse(value);
+ } on FormatException catch (e) {
+ throw CheckedFromJsonException(source, key, 'Pubspec', e.message);
+ }
+
+ return MapEntry(key, constraint);
+ } else {
+ throw CheckedFromJsonException(
+ source,
+ key,
+ 'VersionConstraint',
+ '`$value` is not a String.',
+ );
+ }
+
+ return MapEntry(key, constraint);
+ }) ??
+ {};
diff --git a/pkgs/pubspec_parse/lib/src/pubspec.g.dart b/pkgs/pubspec_parse/lib/src/pubspec.g.dart
new file mode 100644
index 0000000..fc28571
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/pubspec.g.dart
@@ -0,0 +1,64 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+// ignore_for_file: deprecated_member_use_from_same_package, lines_longer_than_80_chars, require_trailing_commas, unnecessary_cast
+
+part of 'pubspec.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+Pubspec _$PubspecFromJson(Map json) => $checkedCreate(
+ 'Pubspec',
+ json,
+ ($checkedConvert) {
+ final val = Pubspec(
+ $checkedConvert('name', (v) => v as String),
+ version: $checkedConvert(
+ 'version', (v) => _versionFromString(v as String?)),
+ publishTo: $checkedConvert('publish_to', (v) => v as String?),
+ author: $checkedConvert('author', (v) => v as String?),
+ authors: $checkedConvert('authors',
+ (v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
+ environment:
+ $checkedConvert('environment', (v) => _environmentMap(v as Map?)),
+ homepage: $checkedConvert('homepage', (v) => v as String?),
+ repository: $checkedConvert(
+ 'repository', (v) => v == null ? null : Uri.parse(v as String)),
+ issueTracker: $checkedConvert('issue_tracker',
+ (v) => v == null ? null : Uri.parse(v as String)),
+ funding: $checkedConvert(
+ 'funding',
+ (v) => (v as List<dynamic>?)
+ ?.map((e) => Uri.parse(e as String))
+ .toList()),
+ topics: $checkedConvert('topics',
+ (v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
+ ignoredAdvisories: $checkedConvert('ignored_advisories',
+ (v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
+ screenshots: $checkedConvert(
+ 'screenshots', (v) => parseScreenshots(v as List?)),
+ documentation: $checkedConvert('documentation', (v) => v as String?),
+ description: $checkedConvert('description', (v) => v as String?),
+ dependencies:
+ $checkedConvert('dependencies', (v) => parseDeps(v as Map?)),
+ devDependencies:
+ $checkedConvert('dev_dependencies', (v) => parseDeps(v as Map?)),
+ dependencyOverrides: $checkedConvert(
+ 'dependency_overrides', (v) => parseDeps(v as Map?)),
+ flutter: $checkedConvert(
+ 'flutter',
+ (v) => (v as Map?)?.map(
+ (k, e) => MapEntry(k as String, e),
+ )),
+ );
+ return val;
+ },
+ fieldKeyMap: const {
+ 'publishTo': 'publish_to',
+ 'issueTracker': 'issue_tracker',
+ 'ignoredAdvisories': 'ignored_advisories',
+ 'devDependencies': 'dev_dependencies',
+ 'dependencyOverrides': 'dependency_overrides'
+ },
+ );
diff --git a/pkgs/pubspec_parse/lib/src/screenshot.dart b/pkgs/pubspec_parse/lib/src/screenshot.dart
new file mode 100644
index 0000000..f5f0be2
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/screenshot.dart
@@ -0,0 +1,65 @@
+// 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 'package:json_annotation/json_annotation.dart';
+
+@JsonSerializable()
+class Screenshot {
+ final String description;
+ final String path;
+
+ Screenshot(this.description, this.path);
+}
+
+List<Screenshot> parseScreenshots(List? input) {
+ final res = <Screenshot>[];
+ if (input == null) {
+ return res;
+ }
+
+ for (final e in input) {
+ if (e is! Map) continue;
+
+ final description = e['description'];
+ if (description == null) {
+ throw CheckedFromJsonException(
+ e,
+ 'description',
+ 'Screenshot',
+ 'Missing required key `description`',
+ );
+ }
+
+ if (description is! String) {
+ throw CheckedFromJsonException(
+ e,
+ 'description',
+ 'Screenshot',
+ '`$description` is not a String',
+ );
+ }
+
+ final path = e['path'];
+ if (path == null) {
+ throw CheckedFromJsonException(
+ e,
+ 'path',
+ 'Screenshot',
+ 'Missing required key `path`',
+ );
+ }
+
+ if (path is! String) {
+ throw CheckedFromJsonException(
+ e,
+ 'path',
+ 'Screenshot',
+ '`$path` is not a String',
+ );
+ }
+
+ res.add(Screenshot(description, path));
+ }
+ return res;
+}
diff --git a/pkgs/pubspec_parse/pubspec.yaml b/pkgs/pubspec_parse/pubspec.yaml
new file mode 100644
index 0000000..bcdaa31
--- /dev/null
+++ b/pkgs/pubspec_parse/pubspec.yaml
@@ -0,0 +1,31 @@
+name: pubspec_parse
+version: 1.4.0-wip
+description: >-
+ Simple package for parsing pubspec.yaml files with a type-safe API and rich
+ error reporting.
+repository: https://github.com/dart-lang/pubspec_parse
+topics:
+- dart-pub
+
+environment:
+ sdk: ^3.2.0
+
+dependencies:
+ checked_yaml: ^2.0.1
+ collection: ^1.15.0
+ json_annotation: ^4.8.0
+ pub_semver: ^2.0.0
+ yaml: ^3.0.0
+
+dev_dependencies:
+ build_runner: ^2.2.1
+ build_verify: ^3.0.0
+ dart_flutter_team_lints: ^3.0.0
+ json_serializable: ^6.6.0
+ path: ^1.8.0
+ # Needed because we are configuring `combining_builder`
+ source_gen: ^1.2.3
+ stack_trace: ^1.10.0
+ test: ^1.21.6
+ test_descriptor: ^2.0.0
+ test_process: ^2.0.0
diff --git a/pkgs/pubspec_parse/test/dependency_test.dart b/pkgs/pubspec_parse/test/dependency_test.dart
new file mode 100644
index 0000000..f1e4f57
--- /dev/null
+++ b/pkgs/pubspec_parse/test/dependency_test.dart
@@ -0,0 +1,446 @@
+// 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:io';
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:pubspec_parse/pubspec_parse.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+void main() {
+ group('hosted', _hostedDependency);
+ group('git', _gitDependency);
+ group('sdk', _sdkDependency);
+ group('path', _pathDependency);
+
+ group('errors', () {
+ test('List', () {
+ _expectThrows(
+ [],
+ r'''
+line 4, column 10: Unsupported value for "dep". Not a valid dependency value.
+ ╷
+4 │ "dep": []
+ │ ^^
+ ╵''',
+ );
+ });
+
+ test('int', () {
+ _expectThrows(
+ 42,
+ r'''
+line 4, column 10: Unsupported value for "dep". Not a valid dependency value.
+ ╷
+4 │ "dep": 42
+ │ ┌──────────^
+5 │ │ }
+ │ └─^
+ ╵''',
+ );
+ });
+
+ test('map with too many keys', () {
+ _expectThrows(
+ {'path': 'a', 'git': 'b'},
+ r'''
+line 6, column 11: Unsupported value for "git". A dependency may only have one source.
+ ╷
+6 │ "git": "b"
+ │ ^^^
+ ╵''',
+ );
+ });
+
+ test('map with unsupported keys', () {
+ _expectThrows(
+ {'bob': 'a', 'jones': 'b'},
+ r'''
+line 5, column 4: Unrecognized keys: [bob]; supported keys: [sdk, git, path, hosted]
+ ╷
+5 │ "bob": "a",
+ │ ^^^^^
+ ╵''',
+ );
+ });
+ });
+}
+
+void _hostedDependency() {
+ test('null', () async {
+ final dep = await _dependency<HostedDependency>(null);
+ expect(dep.version.toString(), 'any');
+ expect(dep.hosted, isNull);
+ expect(dep.toString(), 'HostedDependency: any');
+ });
+
+ test('empty map', () async {
+ final dep = await _dependency<HostedDependency>({});
+ expect(dep.hosted, isNull);
+ expect(dep.toString(), 'HostedDependency: any');
+ });
+
+ test('string version', () async {
+ final dep = await _dependency<HostedDependency>('^1.0.0');
+ expect(dep.version.toString(), '^1.0.0');
+ expect(dep.hosted, isNull);
+ expect(dep.toString(), 'HostedDependency: ^1.0.0');
+ });
+
+ test('bad string version', () {
+ _expectThrows(
+ 'not a version',
+ r'''
+line 4, column 10: Unsupported value for "dep". Could not parse version "not a version". Unknown text at "not a version".
+ ╷
+4 │ "dep": "not a version"
+ │ ^^^^^^^^^^^^^^^
+ ╵''',
+ );
+ });
+
+ test('map w/ just version', () async {
+ final dep = await _dependency<HostedDependency>({'version': '^1.0.0'});
+ expect(dep.version.toString(), '^1.0.0');
+ expect(dep.hosted, isNull);
+ expect(dep.toString(), 'HostedDependency: ^1.0.0');
+ });
+
+ test('map w/ version and hosted as Map', () async {
+ final dep = await _dependency<HostedDependency>({
+ 'version': '^1.0.0',
+ 'hosted': {'name': 'hosted_name', 'url': 'https://hosted_url'},
+ });
+ expect(dep.version.toString(), '^1.0.0');
+ expect(dep.hosted!.name, 'hosted_name');
+ expect(dep.hosted!.url.toString(), 'https://hosted_url');
+ expect(dep.toString(), 'HostedDependency: ^1.0.0');
+ });
+
+ test('map /w hosted as a map without name', () async {
+ final dep = await _dependency<HostedDependency>(
+ {
+ 'version': '^1.0.0',
+ 'hosted': {'url': 'https://hosted_url'},
+ },
+ skipTryPub: true, // todo: Unskip once pub supports this syntax
+ );
+ expect(dep.version.toString(), '^1.0.0');
+ expect(dep.hosted!.declaredName, isNull);
+ expect(dep.hosted!.name, 'dep');
+ expect(dep.hosted!.url.toString(), 'https://hosted_url');
+ expect(dep.toString(), 'HostedDependency: ^1.0.0');
+ });
+
+ test('map w/ bad version value', () {
+ _expectThrows(
+ {
+ 'version': 'not a version',
+ 'hosted': {'name': 'hosted_name', 'url': 'hosted_url'},
+ },
+ r'''
+line 5, column 15: Unsupported value for "version". Could not parse version "not a version". Unknown text at "not a version".
+ ╷
+5 │ "version": "not a version",
+ │ ^^^^^^^^^^^^^^^
+ ╵''',
+ );
+ });
+
+ test('map w/ extra keys should fail', () {
+ _expectThrows(
+ {
+ 'version': '^1.0.0',
+ 'hosted': {'name': 'hosted_name', 'url': 'hosted_url'},
+ 'not_supported': null,
+ },
+ r'''
+line 10, column 4: Unrecognized keys: [not_supported]; supported keys: [sdk, git, path, hosted]
+ ╷
+10 │ "not_supported": null
+ │ ^^^^^^^^^^^^^^^
+ ╵''',
+ );
+ });
+
+ test('map w/ version and hosted as String', () async {
+ final dep = await _dependency<HostedDependency>(
+ {'version': '^1.0.0', 'hosted': 'hosted_url'},
+ skipTryPub: true, // todo: Unskip once put supports this
+ );
+ expect(dep.version.toString(), '^1.0.0');
+ expect(dep.hosted!.declaredName, isNull);
+ expect(dep.hosted!.name, 'dep');
+ expect(dep.hosted!.url, Uri.parse('hosted_url'));
+ expect(dep.toString(), 'HostedDependency: ^1.0.0');
+ });
+
+ test('map w/ hosted as String', () async {
+ final dep = await _dependency<HostedDependency>({'hosted': 'hosted_url'});
+ expect(dep.version, VersionConstraint.any);
+ expect(dep.hosted!.declaredName, isNull);
+ expect(dep.hosted!.name, 'dep');
+ expect(dep.hosted!.url, Uri.parse('hosted_url'));
+ expect(dep.toString(), 'HostedDependency: any');
+ });
+
+ test('map w/ null hosted should error', () {
+ _expectThrows(
+ {'hosted': null},
+ r'''
+line 5, column 4: These keys had `null` values, which is not allowed: [hosted]
+ ╷
+5 │ "hosted": null
+ │ ^^^^^^^^
+ ╵''',
+ );
+ });
+
+ test('map w/ null version is fine', () async {
+ final dep = await _dependency<HostedDependency>({'version': null});
+ expect(dep.version, VersionConstraint.any);
+ expect(dep.hosted, isNull);
+ expect(dep.toString(), 'HostedDependency: any');
+ });
+}
+
+void _sdkDependency() {
+ test('without version', () async {
+ final dep = await _dependency<SdkDependency>({'sdk': 'flutter'});
+ expect(dep.sdk, 'flutter');
+ expect(dep.version, VersionConstraint.any);
+ expect(dep.toString(), 'SdkDependency: flutter');
+ });
+
+ test('with version', () async {
+ final dep = await _dependency<SdkDependency>(
+ {'sdk': 'flutter', 'version': '>=1.2.3 <2.0.0'},
+ );
+ expect(dep.sdk, 'flutter');
+ expect(dep.version.toString(), '>=1.2.3 <2.0.0');
+ expect(dep.toString(), 'SdkDependency: flutter');
+ });
+
+ test('null content', () {
+ _expectThrowsContaining(
+ {'sdk': null},
+ r"type 'Null' is not a subtype of type 'String'",
+ );
+ });
+
+ test('number content', () {
+ _expectThrowsContaining(
+ {'sdk': 42},
+ r"type 'int' is not a subtype of type 'String'",
+ );
+ });
+}
+
+void _gitDependency() {
+ test('string', () async {
+ final dep = await _dependency<GitDependency>({'git': 'url'});
+ expect(dep.url.toString(), 'url');
+ expect(dep.path, isNull);
+ expect(dep.ref, isNull);
+ expect(dep.toString(), 'GitDependency: url@url');
+ });
+
+ test('string with version key is ignored', () async {
+ // Regression test for https://github.com/dart-lang/pubspec_parse/issues/13
+ final dep =
+ await _dependency<GitDependency>({'git': 'url', 'version': '^1.2.3'});
+ expect(dep.url.toString(), 'url');
+ expect(dep.path, isNull);
+ expect(dep.ref, isNull);
+ expect(dep.toString(), 'GitDependency: url@url');
+ });
+
+ test('string with user@ URL', () async {
+ final skipTryParse = Platform.environment.containsKey('TRAVIS');
+ if (skipTryParse) {
+ print('FYI: not validating git@ URI on travis due to failure');
+ }
+ final dep = await _dependency<GitDependency>(
+ {'git': 'git@localhost:dep.git'},
+ skipTryPub: skipTryParse,
+ );
+ expect(dep.url.toString(), 'ssh://git@localhost/dep.git');
+ expect(dep.path, isNull);
+ expect(dep.ref, isNull);
+ expect(dep.toString(), 'GitDependency: url@ssh://git@localhost/dep.git');
+ });
+
+ test('string with random extra key fails', () {
+ _expectThrows(
+ {'git': 'url', 'bob': '^1.2.3'},
+ r'''
+line 6, column 4: Unrecognized keys: [bob]; supported keys: [sdk, git, path, hosted]
+ ╷
+6 │ "bob": "^1.2.3"
+ │ ^^^^^
+ ╵''',
+ );
+ });
+
+ test('map', () async {
+ final dep = await _dependency<GitDependency>({
+ 'git': {'url': 'url', 'path': 'path', 'ref': 'ref'},
+ });
+ expect(dep.url.toString(), 'url');
+ expect(dep.path, 'path');
+ expect(dep.ref, 'ref');
+ expect(dep.toString(), 'GitDependency: url@url');
+ });
+
+ test('git - null content', () {
+ _expectThrows(
+ {'git': null},
+ r'''
+line 5, column 11: Unsupported value for "git". Must be a String or a Map.
+ ╷
+5 │ "git": null
+ │ ┌───────────^
+6 │ │ }
+ │ └──^
+ ╵''',
+ );
+ });
+
+ test('git - int content', () {
+ _expectThrows(
+ {'git': 42},
+ r'''
+line 5, column 11: Unsupported value for "git". Must be a String or a Map.
+ ╷
+5 │ "git": 42
+ │ ┌───────────^
+6 │ │ }
+ │ └──^
+ ╵''',
+ );
+ });
+
+ test('git - empty map', () {
+ _expectThrowsContaining(
+ {'git': <String, dynamic>{}},
+ r"type 'Null' is not a subtype of type 'String'",
+ );
+ });
+
+ test('git - null url', () {
+ _expectThrowsContaining(
+ {
+ 'git': {'url': null},
+ },
+ r"type 'Null' is not a subtype of type 'String'",
+ );
+ });
+
+ test('git - int url', () {
+ _expectThrowsContaining(
+ {
+ 'git': {'url': 42},
+ },
+ r"type 'int' is not a subtype of type 'String'",
+ );
+ });
+}
+
+void _pathDependency() {
+ test('valid', () async {
+ final dep = await _dependency<PathDependency>({'path': '../path'});
+ expect(dep.path, '../path');
+ expect(dep.toString(), 'PathDependency: path@../path');
+ });
+
+ test('valid with version key is ignored', () async {
+ final dep = await _dependency<PathDependency>(
+ {'path': '../path', 'version': '^1.2.3'},
+ );
+ expect(dep.path, '../path');
+ expect(dep.toString(), 'PathDependency: path@../path');
+ });
+
+ test('valid with random extra key fails', () {
+ _expectThrows(
+ {'path': '../path', 'bob': '^1.2.3'},
+ r'''
+line 6, column 4: Unrecognized keys: [bob]; supported keys: [sdk, git, path, hosted]
+ ╷
+6 │ "bob": "^1.2.3"
+ │ ^^^^^
+ ╵''',
+ );
+ });
+
+ test('null content', () {
+ _expectThrows(
+ {'path': null},
+ r'''
+line 5, column 12: Unsupported value for "path". Must be a String.
+ ╷
+5 │ "path": null
+ │ ┌────────────^
+6 │ │ }
+ │ └──^
+ ╵''',
+ );
+ });
+
+ test('int content', () {
+ _expectThrows(
+ {'path': 42},
+ r'''
+line 5, column 12: Unsupported value for "path". Must be a String.
+ ╷
+5 │ "path": 42
+ │ ┌────────────^
+6 │ │ }
+ │ └──^
+ ╵''',
+ );
+ });
+}
+
+void _expectThrows(Object content, String expectedError) {
+ expectParseThrows(
+ {
+ 'name': 'sample',
+ 'dependencies': {'dep': content},
+ },
+ expectedError,
+ );
+}
+
+void _expectThrowsContaining(Object content, String errorText) {
+ expectParseThrowsContaining(
+ {
+ 'name': 'sample',
+ 'dependencies': {'dep': content},
+ },
+ errorText,
+ );
+}
+
+Future<T> _dependency<T extends Dependency>(
+ Object? content, {
+ bool skipTryPub = false,
+}) async {
+ final value = await parse(
+ {
+ ...defaultPubspec,
+ 'dependencies': {'dep': content},
+ },
+ skipTryPub: skipTryPub,
+ );
+ expect(value.name, 'sample');
+ expect(value.dependencies, hasLength(1));
+
+ final entry = value.dependencies.entries.single;
+ expect(entry.key, 'dep');
+
+ return entry.value as T;
+}
diff --git a/pkgs/pubspec_parse/test/ensure_build_test.dart b/pkgs/pubspec_parse/test/ensure_build_test.dart
new file mode 100644
index 0000000..689f6fe
--- /dev/null
+++ b/pkgs/pubspec_parse/test/ensure_build_test.dart
@@ -0,0 +1,15 @@
+// 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.
+
+@Timeout.factor(2)
+@TestOn('vm')
+@Tags(['presubmit-only'])
+library;
+
+import 'package:build_verify/build_verify.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('ensure_build', expectBuildClean);
+}
diff --git a/pkgs/pubspec_parse/test/git_uri_test.dart b/pkgs/pubspec_parse/test/git_uri_test.dart
new file mode 100644
index 0000000..be89ba8
--- /dev/null
+++ b/pkgs/pubspec_parse/test/git_uri_test.dart
@@ -0,0 +1,25 @@
+import 'package:pubspec_parse/src/dependency.dart';
+import 'package:test/test.dart';
+
+void main() {
+ for (var item in {
+ 'git@github.com:google/grinder.dart.git':
+ 'ssh://git@github.com/google/grinder.dart.git',
+ 'host.xz:path/to/repo.git/': 'ssh://host.xz/path/to/repo.git/',
+ 'http:path/to/repo.git/': 'ssh://http/path/to/repo.git/',
+ 'file:path/to/repo.git/': 'ssh://file/path/to/repo.git/',
+ './foo:bar': 'foo%3Abar',
+ '/path/to/repo.git/': '/path/to/repo.git/',
+ 'file:///path/to/repo.git/': 'file:///path/to/repo.git/',
+ }.entries) {
+ test(item.key, () {
+ final uri = parseGitUri(item.key);
+
+ printOnFailure(
+ [uri.scheme, uri.userInfo, uri.host, uri.port, uri.path].join('\n'),
+ );
+
+ expect(uri, Uri.parse(item.value));
+ });
+ }
+}
diff --git a/pkgs/pubspec_parse/test/parse_test.dart b/pkgs/pubspec_parse/test/parse_test.dart
new file mode 100644
index 0000000..6251f41
--- /dev/null
+++ b/pkgs/pubspec_parse/test/parse_test.dart
@@ -0,0 +1,715 @@
+// 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.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+// ignore_for_file: lines_longer_than_80_chars
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+void main() {
+ test('minimal set values', () async {
+ final value = await parse(defaultPubspec);
+ expect(value.name, 'sample');
+ expect(value.version, isNull);
+ expect(value.publishTo, isNull);
+ expect(value.description, isNull);
+ expect(value.homepage, isNull);
+ expect(value.author, isNull);
+ expect(value.authors, isEmpty);
+ expect(
+ value.environment,
+ {'sdk': VersionConstraint.parse('>=2.12.0 <3.0.0')},
+ );
+ expect(value.documentation, isNull);
+ expect(value.dependencies, isEmpty);
+ expect(value.devDependencies, isEmpty);
+ expect(value.dependencyOverrides, isEmpty);
+ expect(value.flutter, isNull);
+ expect(value.repository, isNull);
+ expect(value.issueTracker, isNull);
+ expect(value.screenshots, isEmpty);
+ });
+
+ test('all fields set', () async {
+ final version = Version.parse('1.2.3');
+ final sdkConstraint = VersionConstraint.parse('>=2.12.0 <3.0.0');
+ final value = await parse({
+ 'name': 'sample',
+ 'version': version.toString(),
+ 'publish_to': 'none',
+ 'author': 'name@example.com',
+ 'environment': {'sdk': sdkConstraint.toString()},
+ 'description': 'description',
+ 'homepage': 'homepage',
+ 'documentation': 'documentation',
+ 'repository': 'https://github.com/example/repo',
+ 'issue_tracker': 'https://github.com/example/repo/issues',
+ 'funding': [
+ 'https://patreon.com/example',
+ ],
+ 'topics': ['widget', 'button'],
+ 'ignored_advisories': ['111', '222'],
+ 'screenshots': [
+ {'description': 'my screenshot', 'path': 'path/to/screenshot'},
+ ],
+ });
+ expect(value.name, 'sample');
+ expect(value.version, version);
+ expect(value.publishTo, 'none');
+ expect(value.description, 'description');
+ expect(value.homepage, 'homepage');
+ expect(value.author, 'name@example.com');
+ expect(value.authors, ['name@example.com']);
+ expect(value.environment, hasLength(1));
+ expect(value.environment, containsPair('sdk', sdkConstraint));
+ expect(value.documentation, 'documentation');
+ expect(value.dependencies, isEmpty);
+ expect(value.devDependencies, isEmpty);
+ expect(value.dependencyOverrides, isEmpty);
+ expect(value.repository, Uri.parse('https://github.com/example/repo'));
+ expect(
+ value.issueTracker,
+ Uri.parse('https://github.com/example/repo/issues'),
+ );
+ expect(value.funding, hasLength(1));
+ expect(value.funding!.single.toString(), 'https://patreon.com/example');
+ expect(value.topics, hasLength(2));
+ expect(value.topics!.first, 'widget');
+ expect(value.topics!.last, 'button');
+ expect(value.ignoredAdvisories, hasLength(2));
+ expect(value.ignoredAdvisories!.first, '111');
+ expect(value.ignoredAdvisories!.last, '222');
+ expect(value.screenshots, hasLength(1));
+ expect(value.screenshots!.first.description, 'my screenshot');
+ expect(value.screenshots!.first.path, 'path/to/screenshot');
+ });
+
+ test('environment values can be null', () async {
+ final value = await parse(
+ {
+ 'name': 'sample',
+ 'environment': {
+ 'sdk': '>=2.12.0 <3.0.0',
+ 'bob': null,
+ },
+ },
+ skipTryPub: true,
+ );
+ expect(value.name, 'sample');
+ expect(value.environment, hasLength(2));
+ expect(value.environment, containsPair('bob', isNull));
+ });
+
+ group('publish_to', () {
+ for (var entry in {
+ 42: "Unsupported value for \"publish_to\". type 'int' is not a subtype of type 'String?'",
+ '##not a uri!': r'''
+line 3, column 16: Unsupported value for "publish_to". Must be an http or https URL.
+ ╷
+3 │ "publish_to": "##not a uri!"
+ │ ^^^^^^^^^^^^^^
+ ╵''',
+ '/cool/beans': r'''
+line 3, column 16: Unsupported value for "publish_to". Must be an http or https URL.
+ ╷
+3 │ "publish_to": "/cool/beans"
+ │ ^^^^^^^^^^^^^
+ ╵''',
+ 'file:///Users/kevmoo/': r'''
+line 3, column 16: Unsupported value for "publish_to". Must be an http or https URL.
+ ╷
+3 │ "publish_to": "file:///Users/kevmoo/"
+ │ ^^^^^^^^^^^^^^^^^^^^^^^
+ ╵''',
+ }.entries) {
+ test('cannot be `${entry.key}`', () {
+ expectParseThrowsContaining(
+ {'name': 'sample', 'publish_to': entry.key},
+ entry.value,
+ skipTryPub: true,
+ );
+ });
+ }
+
+ for (var entry in {
+ null: null,
+ 'http': 'http://example.com',
+ 'https': 'https://example.com',
+ 'none': 'none',
+ }.entries) {
+ test('can be ${entry.key}', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'publish_to': entry.value,
+ });
+ expect(value.publishTo, entry.value);
+ });
+ }
+ });
+
+ group('author, authors', () {
+ test('one author', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'author': 'name@example.com',
+ });
+ expect(value.author, 'name@example.com');
+ expect(value.authors, ['name@example.com']);
+ });
+
+ test('one author, via authors', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'authors': ['name@example.com'],
+ });
+ expect(value.author, 'name@example.com');
+ expect(value.authors, ['name@example.com']);
+ });
+
+ test('many authors', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'authors': ['name@example.com', 'name2@example.com'],
+ });
+ expect(value.author, isNull);
+ expect(value.authors, ['name@example.com', 'name2@example.com']);
+ });
+
+ test('author and authors', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'author': 'name@example.com',
+ 'authors': ['name2@example.com'],
+ });
+ expect(value.author, isNull);
+ expect(value.authors, ['name@example.com', 'name2@example.com']);
+ });
+
+ test('duplicate author values', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'author': 'name@example.com',
+ 'authors': ['name@example.com', 'name@example.com'],
+ });
+ expect(value.author, 'name@example.com');
+ expect(value.authors, ['name@example.com']);
+ });
+
+ test('flutter', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'flutter': {'key': 'value'},
+ });
+ expect(value.flutter, {'key': 'value'});
+ });
+ });
+
+ group('invalid', () {
+ test('null', () {
+ expectParseThrows(
+ null,
+ r'''
+line 1, column 1: Not a map
+ ╷
+1 │ null
+ │ ^^^^
+ ╵''',
+ );
+ });
+ test('empty string', () {
+ expectParseThrows(
+ '',
+ r'''
+line 1, column 1: Not a map
+ ╷
+1 │ ""
+ │ ^^
+ ╵''',
+ );
+ });
+ test('array', () {
+ expectParseThrows(
+ [],
+ r'''
+line 1, column 1: Not a map
+ ╷
+1 │ []
+ │ ^^
+ ╵''',
+ );
+ });
+
+ test('missing name', () {
+ expectParseThrowsContaining(
+ {},
+ "Missing key \"name\". type 'Null' is not a subtype of type 'String'",
+ );
+ });
+
+ test('null name value', () {
+ expectParseThrowsContaining(
+ {'name': null},
+ "Unsupported value for \"name\". type 'Null' is not a subtype of type 'String'",
+ );
+ });
+
+ test('empty name value', () {
+ expectParseThrows(
+ {'name': ''},
+ r'''
+line 2, column 10: Unsupported value for "name". "name" cannot be empty.
+ ╷
+2 │ "name": ""
+ │ ^^
+ ╵''',
+ );
+ });
+
+ test('"dart" is an invalid environment key', () {
+ expectParseThrows(
+ {
+ 'name': 'sample',
+ 'environment': {'dart': 'cool'},
+ },
+ r'''
+line 4, column 3: Use "sdk" to for Dart SDK constraints.
+ ╷
+4 │ "dart": "cool"
+ │ ^^^^^^
+ ╵''',
+ );
+ });
+
+ test('environment values cannot be int', () {
+ expectParseThrows(
+ {
+ 'name': 'sample',
+ 'environment': {'sdk': 42},
+ },
+ r'''
+line 4, column 10: Unsupported value for "sdk". `42` is not a String.
+ ╷
+4 │ "sdk": 42
+ │ ┌──────────^
+5 │ │ }
+ │ └─^
+ ╵''',
+ );
+ });
+
+ test('version', () {
+ expectParseThrows(
+ {'name': 'sample', 'version': 'invalid'},
+ r'''
+line 3, column 13: Unsupported value for "version". Could not parse "invalid".
+ ╷
+3 │ "version": "invalid"
+ │ ^^^^^^^^^
+ ╵''',
+ );
+ });
+
+ test('invalid environment value', () {
+ expectParseThrows(
+ {
+ 'name': 'sample',
+ 'environment': {'sdk': 'silly'},
+ },
+ r'''
+line 4, column 10: Unsupported value for "sdk". Could not parse version "silly". Unknown text at "silly".
+ ╷
+4 │ "sdk": "silly"
+ │ ^^^^^^^
+ ╵''',
+ );
+ });
+
+ test('bad repository url', () {
+ expectParseThrowsContaining(
+ {
+ ...defaultPubspec,
+ 'repository': {'x': 'y'},
+ },
+ "Unsupported value for \"repository\". type 'YamlMap' is not a subtype of type 'String'",
+ skipTryPub: true,
+ );
+ });
+
+ test('bad issue_tracker url', () {
+ expectParseThrowsContaining(
+ {
+ 'name': 'sample',
+ 'issue_tracker': {'x': 'y'},
+ },
+ "Unsupported value for \"issue_tracker\". type 'YamlMap' is not a subtype of type 'String'",
+ skipTryPub: true,
+ );
+ });
+ });
+
+ group('funding', () {
+ test('not a list', () {
+ expectParseThrowsContaining(
+ {
+ ...defaultPubspec,
+ 'funding': 1,
+ },
+ "Unsupported value for \"funding\". type 'int' is not a subtype of type 'List<dynamic>?'",
+ skipTryPub: true,
+ );
+ });
+
+ test('not an uri', () {
+ expectParseThrowsContaining(
+ {
+ ...defaultPubspec,
+ 'funding': [1],
+ },
+ "Unsupported value for \"funding\". type 'int' is not a subtype of type 'String'",
+ skipTryPub: true,
+ );
+ });
+
+ test('not an uri', () {
+ expectParseThrows(
+ {
+ ...defaultPubspec,
+ 'funding': ['ht tps://example.com/'],
+ },
+ r'''
+line 6, column 13: Unsupported value for "funding". Illegal scheme character at offset 2.
+ ╷
+6 │ "funding": [
+ │ ┌─────────────^
+7 │ │ "ht tps://example.com/"
+8 │ └ ]
+ ╵''',
+ skipTryPub: true,
+ );
+ });
+ });
+ group('topics', () {
+ test('not a list', () {
+ expectParseThrowsContaining(
+ {
+ ...defaultPubspec,
+ 'topics': 1,
+ },
+ "Unsupported value for \"topics\". type 'int' is not a subtype of type 'List<dynamic>?'",
+ skipTryPub: true,
+ );
+ });
+
+ test('not a string', () {
+ expectParseThrowsContaining(
+ {
+ ...defaultPubspec,
+ 'topics': [1],
+ },
+ "Unsupported value for \"topics\". type 'int' is not a subtype of type 'String'",
+ skipTryPub: true,
+ );
+ });
+
+ test('invalid data - lenient', () async {
+ final value = await parse(
+ {
+ ...defaultPubspec,
+ 'topics': [1],
+ },
+ skipTryPub: true,
+ lenient: true,
+ );
+ expect(value.name, 'sample');
+ expect(value.topics, isNull);
+ });
+ });
+
+ group('ignored_advisories', () {
+ test('not a list', () {
+ expectParseThrowsContaining(
+ {
+ ...defaultPubspec,
+ 'ignored_advisories': 1,
+ },
+ "Unsupported value for \"ignored_advisories\". type 'int' is not a subtype of type 'List<dynamic>?'",
+ skipTryPub: true,
+ );
+ });
+
+ test('not a string', () {
+ expectParseThrowsContaining(
+ {
+ ...defaultPubspec,
+ 'ignored_advisories': [1],
+ },
+ "Unsupported value for \"ignored_advisories\". type 'int' is not a subtype of type 'String'",
+ skipTryPub: true,
+ );
+ });
+
+ test('invalid data - lenient', () async {
+ final value = await parse(
+ {
+ ...defaultPubspec,
+ 'ignored_advisories': [1],
+ },
+ skipTryPub: true,
+ lenient: true,
+ );
+ expect(value.name, 'sample');
+ expect(value.ignoredAdvisories, isNull);
+ });
+ });
+
+ group('screenshots', () {
+ test('one screenshot', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'screenshots': [
+ {'description': 'my screenshot', 'path': 'path/to/screenshot'},
+ ],
+ });
+ expect(value.screenshots, hasLength(1));
+ expect(value.screenshots!.first.description, 'my screenshot');
+ expect(value.screenshots!.first.path, 'path/to/screenshot');
+ });
+
+ test('many screenshots', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'screenshots': [
+ {'description': 'my screenshot', 'path': 'path/to/screenshot'},
+ {
+ 'description': 'my second screenshot',
+ 'path': 'path/to/screenshot2',
+ },
+ ],
+ });
+ expect(value.screenshots, hasLength(2));
+ expect(value.screenshots!.first.description, 'my screenshot');
+ expect(value.screenshots!.first.path, 'path/to/screenshot');
+ expect(value.screenshots!.last.description, 'my second screenshot');
+ expect(value.screenshots!.last.path, 'path/to/screenshot2');
+ });
+
+ test('one screenshot plus invalid entries', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'screenshots': [
+ 42,
+ {
+ 'description': 'my screenshot',
+ 'path': 'path/to/screenshot',
+ 'extraKey': 'not important',
+ },
+ 'not a screenshot',
+ ],
+ });
+ expect(value.screenshots, hasLength(1));
+ expect(value.screenshots!.first.description, 'my screenshot');
+ expect(value.screenshots!.first.path, 'path/to/screenshot');
+ });
+
+ test('invalid entries', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'screenshots': [
+ 42,
+ 'not a screenshot',
+ ],
+ });
+ expect(value.screenshots, isEmpty);
+ });
+
+ test('missing key `dessription', () {
+ expectParseThrows(
+ {
+ ...defaultPubspec,
+ 'screenshots': [
+ {'path': 'my/path'},
+ ],
+ },
+ r'''
+line 7, column 3: Missing key "description". Missing required key `description`
+ ╷
+7 │ ┌ {
+8 │ │ "path": "my/path"
+9 │ └ }
+ ╵''',
+ skipTryPub: true,
+ );
+ });
+
+ test('missing key `path`', () {
+ expectParseThrows(
+ {
+ ...defaultPubspec,
+ 'screenshots': [
+ {'description': 'my screenshot'},
+ ],
+ },
+ r'''
+line 7, column 3: Missing key "path". Missing required key `path`
+ ╷
+7 │ ┌ {
+8 │ │ "description": "my screenshot"
+9 │ └ }
+ ╵''',
+ skipTryPub: true,
+ );
+ });
+
+ test('Value of description not a String`', () {
+ expectParseThrows(
+ {
+ ...defaultPubspec,
+ 'screenshots': [
+ {'description': 42},
+ ],
+ },
+ r'''
+line 8, column 19: Unsupported value for "description". `42` is not a String
+ ╷
+8 │ "description": 42
+ │ ┌───────────────────^
+9 │ │ }
+ │ └──^
+ ╵''',
+ skipTryPub: true,
+ );
+ });
+
+ test('Value of path not a String`', () {
+ expectParseThrows(
+ {
+ ...defaultPubspec,
+ 'screenshots': [
+ {
+ 'description': '',
+ 'path': 42,
+ },
+ ],
+ },
+ r'''
+line 9, column 12: Unsupported value for "path". `42` is not a String
+ ╷
+9 │ "path": 42
+ │ ┌────────────^
+10 │ │ }
+ │ └──^
+ ╵''',
+ skipTryPub: true,
+ );
+ });
+
+ test('invalid screenshot - lenient', () async {
+ final value = await parse(
+ {
+ ...defaultPubspec,
+ 'screenshots': 'Invalid value',
+ },
+ lenient: true,
+ );
+ expect(value.name, 'sample');
+ expect(value.screenshots, isEmpty);
+ });
+ });
+
+ group('lenient', () {
+ test('null', () {
+ expectParseThrows(
+ null,
+ r'''
+line 1, column 1: Not a map
+ ╷
+1 │ null
+ │ ^^^^
+ ╵''',
+ lenient: true,
+ );
+ });
+
+ test('empty string', () {
+ expectParseThrows(
+ '',
+ r'''
+line 1, column 1: Not a map
+ ╷
+1 │ ""
+ │ ^^
+ ╵''',
+ lenient: true,
+ );
+ });
+
+ test('name cannot be empty', () {
+ expectParseThrowsContaining(
+ {},
+ "Missing key \"name\". type 'Null' is not a subtype of type 'String'",
+ lenient: true,
+ );
+ });
+
+ test('bad repository url', () async {
+ final value = await parse(
+ {
+ ...defaultPubspec,
+ 'repository': {'x': 'y'},
+ },
+ lenient: true,
+ );
+ expect(value.name, 'sample');
+ expect(value.repository, isNull);
+ });
+
+ test('bad issue_tracker url', () async {
+ final value = await parse(
+ {
+ ...defaultPubspec,
+ 'issue_tracker': {'x': 'y'},
+ },
+ lenient: true,
+ );
+ expect(value.name, 'sample');
+ expect(value.issueTracker, isNull);
+ });
+
+ test('multiple bad values', () async {
+ final value = await parse(
+ {
+ ...defaultPubspec,
+ 'repository': {'x': 'y'},
+ 'issue_tracker': {'x': 'y'},
+ },
+ lenient: true,
+ );
+ expect(value.name, 'sample');
+ expect(value.repository, isNull);
+ expect(value.issueTracker, isNull);
+ });
+
+ test('deep error throws with lenient', () {
+ expect(
+ () => parse(
+ {
+ 'name': 'sample',
+ 'dependencies': {
+ 'foo': {
+ 'git': {'url': 1},
+ },
+ },
+ 'issue_tracker': {'x': 'y'},
+ },
+ skipTryPub: true,
+ lenient: true,
+ ),
+ throwsException,
+ );
+ });
+ });
+}
diff --git a/pkgs/pubspec_parse/test/pub_utils.dart b/pkgs/pubspec_parse/test/pub_utils.dart
new file mode 100644
index 0000000..a60aa2a
--- /dev/null
+++ b/pkgs/pubspec_parse/test/pub_utils.dart
@@ -0,0 +1,88 @@
+// 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:async';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+import 'package:test_process/test_process.dart';
+
+Future<ProcResult> tryPub(String content) async {
+ await d.file('pubspec.yaml', content).create();
+
+ final proc = await TestProcess.start(
+ Platform.resolvedExecutable,
+ ['pub', 'get', '--offline'],
+ workingDirectory: d.sandbox,
+ // Don't pass current process VM options to child
+ environment: Map.from(Platform.environment)..remove('DART_VM_OPTIONS'),
+ );
+
+ final result = await ProcResult.fromTestProcess(proc);
+
+ printOnFailure(
+ [
+ '-----BEGIN pub output-----',
+ result.toString().trim(),
+ '-----END pub output-----',
+ ].join('\n'),
+ );
+
+ if (result.exitCode == 0) {
+ final lockContent =
+ File(p.join(d.sandbox, 'pubspec.lock')).readAsStringSync();
+
+ printOnFailure(
+ [
+ '-----BEGIN pubspec.lock-----',
+ lockContent.trim(),
+ '-----END pubspec.lock-----',
+ ].join('\n'),
+ );
+ }
+
+ return result;
+}
+
+class ProcResult {
+ final int exitCode;
+ final List<ProcLine> lines;
+
+ bool get cleanParse => exitCode == 0 || exitCode == 66 || exitCode == 69;
+
+ ProcResult(this.exitCode, this.lines);
+
+ static Future<ProcResult> fromTestProcess(TestProcess proc) async {
+ final items = <ProcLine>[];
+
+ final values = await Future.wait([
+ proc.exitCode,
+ proc.stdoutStream().forEach((line) => items.add(ProcLine(false, line))),
+ proc.stderrStream().forEach((line) => items.add(ProcLine(true, line))),
+ ]);
+
+ return ProcResult(values[0] as int, items);
+ }
+
+ @override
+ String toString() {
+ final buffer = StringBuffer('Exit code: $exitCode');
+ for (var line in lines) {
+ buffer.write('\n$line');
+ }
+ return buffer.toString();
+ }
+}
+
+class ProcLine {
+ final bool isError;
+ final String line;
+
+ ProcLine(this.isError, this.line);
+
+ @override
+ String toString() => '${isError ? 'err' : 'out'} $line';
+}
diff --git a/pkgs/pubspec_parse/test/test_utils.dart b/pkgs/pubspec_parse/test/test_utils.dart
new file mode 100644
index 0000000..cc46522
--- /dev/null
+++ b/pkgs/pubspec_parse/test/test_utils.dart
@@ -0,0 +1,157 @@
+// 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:convert';
+
+import 'package:checked_yaml/checked_yaml.dart';
+import 'package:json_annotation/json_annotation.dart';
+import 'package:pubspec_parse/pubspec_parse.dart';
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import 'pub_utils.dart';
+
+const defaultPubspec = {
+ 'name': 'sample',
+ 'environment': {'sdk': '>=2.12.0 <3.0.0'},
+};
+
+String _encodeJson(Object? input) =>
+ const JsonEncoder.withIndent(' ').convert(input);
+
+Matcher _throwsParsedYamlException(String prettyValue) => throwsA(
+ const TypeMatcher<ParsedYamlException>().having(
+ (e) {
+ final message = e.formattedMessage;
+ printOnFailure("Actual error format:\nr'''\n$message'''");
+ _printDebugParsedYamlException(e);
+ return message;
+ },
+ 'formattedMessage',
+ prettyValue,
+ ),
+ );
+
+void _printDebugParsedYamlException(ParsedYamlException e) {
+ var innerError = e.innerError;
+ StackTrace? innerStack;
+
+ if (innerError is CheckedFromJsonException) {
+ final cfje = innerError;
+
+ if (cfje.innerError != null) {
+ innerError = cfje.innerError;
+ innerStack = cfje.innerStack;
+ }
+ }
+
+ if (innerError != null) {
+ final items = [innerError];
+ if (innerStack != null) {
+ items.add(Trace.format(innerStack));
+ }
+
+ final content =
+ LineSplitter.split(items.join('\n')).map((e) => ' $e').join('\n');
+
+ printOnFailure('Inner error details:\n$content');
+ }
+}
+
+Future<Pubspec> parse(
+ Object? content, {
+ bool quietOnError = false,
+ bool skipTryPub = false,
+ bool lenient = false,
+}) async {
+ final encoded = _encodeJson(content);
+
+ ProcResult? pubResult;
+ if (!skipTryPub) {
+ // ignore: deprecated_member_use
+ pubResult = await tryPub(encoded);
+ expect(pubResult, isNotNull);
+ }
+
+ try {
+ final value = Pubspec.parse(encoded, lenient: lenient);
+
+ if (pubResult != null) {
+ addTearDown(() {
+ expect(
+ pubResult!.cleanParse,
+ isTrue,
+ reason:
+ 'On success, parsing from the pub client should also succeed.',
+ );
+ });
+ }
+ return value;
+ } catch (e) {
+ if (pubResult != null) {
+ addTearDown(() {
+ expect(
+ pubResult!.cleanParse,
+ isFalse,
+ reason: 'On failure, parsing from the pub client should also fail.',
+ );
+ });
+ }
+ if (e is ParsedYamlException) {
+ if (!quietOnError) {
+ _printDebugParsedYamlException(e);
+ }
+ }
+ rethrow;
+ }
+}
+
+void expectParseThrows(
+ Object? content,
+ String expectedError, {
+ bool skipTryPub = false,
+ bool lenient = false,
+}) =>
+ expect(
+ () => parse(
+ content,
+ lenient: lenient,
+ quietOnError: true,
+ skipTryPub: skipTryPub,
+ ),
+ _throwsParsedYamlException(expectedError),
+ );
+
+void expectParseThrowsContaining(
+ Object? content,
+ String errorFragment, {
+ bool skipTryPub = false,
+ bool lenient = false,
+}) {
+ expect(
+ () => parse(
+ content,
+ lenient: lenient,
+ quietOnError: true,
+ skipTryPub: skipTryPub,
+ ),
+ _throwsParsedYamlExceptionContaining(errorFragment),
+ );
+}
+
+// ignore: prefer_expression_function_bodies
+Matcher _throwsParsedYamlExceptionContaining(String errorFragment) {
+ return throwsA(
+ const TypeMatcher<ParsedYamlException>().having(
+ (e) {
+ final message = e.formattedMessage;
+ printOnFailure("Actual error format:\nr'''\n$message'''");
+ _printDebugParsedYamlException(e);
+ return message;
+ },
+ 'formattedMessage',
+ contains(errorFragment),
+ ),
+ );
+}