diff --git a/pkgs/pubspec_parse/lib/src/dependency.dart b/pkgs/pubspec_parse/lib/src/dependency.dart index ff052fa64..416fcbaea 100644 --- a/pkgs/pubspec_parse/lib/src/dependency.dart +++ b/pkgs/pubspec_parse/lib/src/dependency.dart @@ -2,7 +2,6 @@ // 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'; @@ -46,18 +45,24 @@ Dependency? _fromJson(Object? data, String name) { } if (data is Map) { - final matchedKeys = data.keys - .cast() - .where((key) => key != 'version') - .toList(); + String? matchedKey; + String? secondMatchedKey; + String? firstUnrecognizedKey; + for (final (key as String) in data.keys) { + if (key == 'version') continue; + if (matchedKey == null) { + matchedKey = key; + } else { + secondMatchedKey ??= key; + } + if (!_sourceKeys.contains(key)) { + firstUnrecognizedKey ??= key; + } + } - if (data.isEmpty || (matchedKeys.isEmpty && data.containsKey('version'))) { + if (matchedKey == null) { return _$HostedDependencyFromJson(data); } else { - final firstUnrecognizedKey = matchedKeys.firstWhereOrNull( - (k) => !_sourceKeys.contains(k), - ); - return $checkedNew('Dependency', data, () { if (firstUnrecognizedKey != null) { throw UnrecognizedKeysException( @@ -66,20 +71,18 @@ Dependency? _fromJson(Object? data, String name) { _sourceKeys, ); } - if (matchedKeys.length > 1) { + if (secondMatchedKey != null) { throw CheckedFromJsonException( data, - matchedKeys[1], + secondMatchedKey, '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]), + return switch (matchedKey) { + 'git' => GitDependency.fromData(data[matchedKey]), + 'path' => PathDependency.fromData(data[matchedKey]), 'sdk' => _$SdkDependencyFromJson(data), 'hosted' => _$HostedDependencyFromJson( data, @@ -125,17 +128,8 @@ class GitDependency extends Dependency { 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.'); - } + factory GitDependency.fromData(Object? data) => + _$GitDependencyFromJson(_mapOrStringUri(data, 'git')); @override bool operator ==(Object other) => @@ -156,34 +150,37 @@ Uri? parseGitUriOrNull(String? value) => Uri parseGitUri(String value) => _tryParseScpUri(value) ?? Uri.parse(value); -/// Supports URIs like `[user@]host.xz:path/to/repo.git/` +/// Parses 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); + // Find first `:`. Remember `@` before it, reject if `/` before it. + const slashChar = 0x2F, colonChar = 0x3A, atChar = 0x40; + var atIndex = -1; + for (var i = 0; i < value.length; i++) { + final char = value.codeUnitAt(i); + if (char == slashChar) { + // 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 + break; + } else if (char == atChar) { + atIndex = i; + } else if (char == colonChar) { + final colonIndex = i; + + // Assume a `://` means it's a real URI scheme and authority, + // not an SCP-like URI. + if (value.startsWith('//', colonIndex + 1)) return null; + + 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); + } } + // No colon in value, or not before a slash. return null; } @@ -257,17 +254,8 @@ class HostedDetails { 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.'); - } + factory HostedDetails.fromJson(Object data) => + _$HostedDetailsFromJson(_mapOrStringUri(data, 'hosted')); @override bool operator ==(Object other) => @@ -279,3 +267,18 @@ class HostedDetails { VersionConstraint _constraintFromString(String? input) => input == null ? VersionConstraint.any : VersionConstraint.parse(input); + +/// The `value` if it is a `Map`, or `{'uri': value}` if it is a `String`. +/// +/// Must be one or the other. +/// The [name] is used as parameter name in the thrown error. +Map _mapOrStringUri(Object? value, String name) { + if (value is! Map) { + if (value is String) { + value = {'url': value}; + } else { + throw ArgumentError.value(value, name, 'Must be a String or a Map.'); + } + } + return value; +} diff --git a/pkgs/pubspec_parse/lib/src/pubspec.dart b/pkgs/pubspec_parse/lib/src/pubspec.dart index 01327f4a3..776b33b45 100644 --- a/pkgs/pubspec_parse/lib/src/pubspec.dart +++ b/pkgs/pubspec_parse/lib/src/pubspec.dart @@ -60,7 +60,7 @@ class Pubspec { @Deprecated('See https://dart.dev/tools/pub/pubspec#authorauthors') String? get author { if (authors.length == 1) { - return authors.single; + return authors.first; } return null; } @@ -140,9 +140,9 @@ class Pubspec { throw ArgumentError.value(name, 'name', '"name" cannot be empty.'); } - if (publishTo != null && publishTo != 'none') { + if (publishTo case final publishTo? when publishTo != 'none') { try { - final targetUri = Uri.parse(publishTo!); + final targetUri = Uri.parse(publishTo); if (!(targetUri.isScheme('http') || targetUri.isScheme('https'))) { throw const FormatException('Must be an http or https URL.'); } @@ -160,10 +160,10 @@ class Pubspec { return _$PubspecFromJson(json); } on CheckedFromJsonException catch (e) { if (e.map == json && json.containsKey(e.key)) { - json = Map.from(json)..remove(e.key); - continue; + json = {...json}..remove(e.key); + } else { + rethrow; } - rethrow; } } } @@ -183,67 +183,63 @@ class Pubspec { ); static List _normalizeAuthors(String? author, List? authors) { - final value = {if (author != null) author, ...?authors}; - return value.toList(); + if (author == null) return authors ?? const []; + return [ + author, + if (authors != null) + for (var otherAuthor in authors) + if (author != otherAuthor) otherAuthor, + ]; } } Version? _versionFromString(String? input) => input == null ? null : Version.parse(input); -Map _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); - } +Map _environmentMap(Map? source) { + if (source == null) return {}; + return 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, + ); + } - return MapEntry(key, constraint); - } else { - throw CheckedFromJsonException( - source, - key, - 'VersionConstraint', - '`$value` is not a String.', - ); + VersionConstraint? constraint; + if (value is String) { + try { + constraint = VersionConstraint.parse(value); + } on FormatException catch (e) { + throw CheckedFromJsonException(source, key, 'Pubspec', e.message); } + } else if (value != null) { + throw CheckedFromJsonException( + source, + key, + 'VersionConstraint', + '`$value` is not a String.', + ); + } + return MapEntry(key, constraint); + }); +} - return MapEntry(key, constraint); - }) ?? - {}; - -Map _executablesMap(Map? source) => - source?.map((k, value) { - final key = k as String; - if (value == null) { - return MapEntry(key, null); - } else if (value is String) { - return MapEntry(key, value); - } else { - throw CheckedFromJsonException( - source, - key, - 'String', - '`$value` is not a String.', - ); - } - }) ?? - {}; +Map _executablesMap(Map? source) => { + if (source != null) + for (final MapEntry(:key as String, :value) in source.entries) + key: (value is String?) + ? value + : (throw CheckedFromJsonException( + source, + key, + 'String', + '`$value` is not a String.', + )), +}; diff --git a/pkgs/pubspec_parse/lib/src/screenshot.dart b/pkgs/pubspec_parse/lib/src/screenshot.dart index f5f0be2ea..99a902cb6 100644 --- a/pkgs/pubspec_parse/lib/src/screenshot.dart +++ b/pkgs/pubspec_parse/lib/src/screenshot.dart @@ -12,54 +12,28 @@ class Screenshot { Screenshot(this.description, this.path); } -List parseScreenshots(List? input) { - final res = []; - if (input == null) { - return res; +List parseScreenshots(List? input) => [ + if (input != null) + for (final e in input) + if (e is Map) + Screenshot(_readString(e, 'description'), _readString(e, 'path')), +]; + +String _readString(Map input, String entryName) { + final value = input[entryName]; + if (value is String) return value; + if (value == null) { + throw CheckedFromJsonException( + input, + entryName, + 'Screenshot', + 'Missing required key `$entryName`', + ); } - - 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; + throw CheckedFromJsonException( + input, + entryName, + 'Screenshot', + '`$value` is not a String', + ); }