diff --git a/lib/src/git.dart b/lib/src/git.dart index 0633169..5fa7536 100644 --- a/lib/src/git.dart +++ b/lib/src/git.dart
@@ -5,6 +5,8 @@ /// Helper functionality for invoking Git. import 'dart:async'; +import 'package:path/path.dart' as p; + import 'exceptions.dart'; import 'io.dart'; import 'log.dart' as log; @@ -103,6 +105,22 @@ String? _commandCache; +/// Returns the root of the git repo [dir] belongs to. Returns `null` if not +/// in a git repo or git is not installed. +String? repoRoot(String dir) { + if (isInstalled) { + try { + return p.normalize( + runSync(['rev-parse', '--show-toplevel'], workingDir: dir).first, + ); + } on GitException { + // Not in a git folder. + return null; + } + } + return null; +} + /// Checks whether [command] is the Git command for this computer. bool _tryGitCommand(String command) { // If "git --version" prints something familiar, git is working.
diff --git a/lib/src/package.dart b/lib/src/package.dart index 6a3b0ff..1aeb9dc 100644 --- a/lib/src/package.dart +++ b/lib/src/package.dart
@@ -224,16 +224,7 @@ // An in-memory package has no files. if (dir == null) return []; - var root = dir; - if (git.isInstalled) { - try { - root = p.normalize( - git.runSync(['rev-parse', '--show-toplevel'], workingDir: dir).first, - ); - } on git.GitException { - // Not in a git folder. - } - } + var root = git.repoRoot(dir) ?? dir; beneath = p .toUri(p.normalize(p.relative(p.join(dir, beneath ?? '.'), from: root))) .path; @@ -279,7 +270,7 @@ : (fileExists(gitIgnore) ? gitIgnore : null); final rules = [ - if (dir == '.') ..._basicIgnoreRules, + if (dir == beneath) ..._basicIgnoreRules, if (ignoreFile != null) readTextFile(ignoreFile), ]; return rules.isEmpty
diff --git a/lib/src/package_config.dart b/lib/src/package_config.dart index fad20f7..0493dcc 100644 --- a/lib/src/package_config.dart +++ b/lib/src/package_config.dart
@@ -2,10 +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. -// @dart=2.10 - -import 'package:meta/meta.dart'; - import 'package:pub_semver/pub_semver.dart'; import 'language_version.dart'; @@ -22,38 +18,36 @@ /// Date-time the `.dart_tool/package_config.json` file was generated. /// - /// This property is **optional** and may be `null` if not given. - DateTime generated; + /// `null` if not given. + DateTime? generated; /// Tool that generated the `.dart_tool/package_config.json` file. /// /// For `pub` this is always `'pub'`. /// - /// This property is **optional** and may be `null` if not given. - String generator; + /// `null` if not given. + String? generator; /// Version of the tool that generated the `.dart_tool/package_config.json` /// file. /// /// For `pub` this is the Dart SDK version from which `pub get` was called. /// - /// This property is **optional** and may be `null` if not given. - Version generatorVersion; + /// `null` if not given. + Version? generatorVersion; /// Additional properties not in the specification for the /// `.dart_tool/package_config.json` file. Map<String, dynamic> additionalProperties; PackageConfig({ - @required this.configVersion, - @required this.packages, + required this.configVersion, + required this.packages, this.generated, this.generator, this.generatorVersion, - this.additionalProperties, - }) { - additionalProperties ??= {}; - } + Map<String, dynamic>? additionalProperties, + }) : additionalProperties = additionalProperties ?? {}; /// Create [PackageConfig] from JSON [data]. /// @@ -63,7 +57,7 @@ if (data is! Map<String, dynamic>) { throw FormatException('package_config.json must be a JSON object'); } - final root = data as Map<String, dynamic>; + final root = data; void _throw(String property, String mustBe) => throw FormatException( '"$property" in .dart_tool/package_config.json $mustBe'); @@ -87,7 +81,7 @@ } // Read the 'generated' property - DateTime generated; + DateTime? generated; final generatedRaw = root['generated']; if (generatedRaw != null) { if (generatedRaw is! String) { @@ -104,7 +98,7 @@ } // Read the 'generatorVersion' property - Version generatorVersion; + Version? generatorVersion; final generatorVersionRaw = root['generatorVersion']; if (generatorVersionRaw != null) { if (generatorVersionRaw is! String) { @@ -134,13 +128,13 @@ } /// Convert to JSON structure. - Map<String, Object> toJson() => { + Map<String, Object?> toJson() => { 'configVersion': configVersion, 'packages': packages.map((p) => p.toJson()).toList(), - 'generated': generated?.toUtc()?.toIso8601String(), + 'generated': generated?.toUtc().toIso8601String(), 'generator': generator, 'generatorVersion': generatorVersion?.toString(), - }..addAll(additionalProperties ?? {}); + }..addAll(additionalProperties); } class PackageConfigEntry { @@ -158,8 +152,8 @@ /// Import statements in Dart programs are resolved relative to this folder. /// This must be in the sub-tree under [rootUri]. /// - /// This property is **optional** and may be `null` if not given. - Uri packageUri; + /// `null` if not given. + Uri? packageUri; /// Language version used by package. /// @@ -167,16 +161,16 @@ /// comment. This is derived from the lower-bound on the Dart SDK requirement /// in the `pubspec.yaml` for the given package. /// - /// This property is **optional** and may be `null` if not given. - LanguageVersion languageVersion; + /// `null` if not given. + LanguageVersion? languageVersion; /// Additional properties not in the specification for the /// `.dart_tool/package_config.json` file. - Map<String, dynamic> additionalProperties; + Map<String, dynamic>? additionalProperties; PackageConfigEntry({ - @required this.name, - @required this.rootUri, + required this.name, + required this.rootUri, this.packageUri, this.languageVersion, this.additionalProperties, @@ -193,9 +187,9 @@ throw FormatException( 'packages[] entries in package_config.json must be JSON objects'); } - final root = data as Map<String, dynamic>; + final root = data; - void _throw(String property, String mustBe) => throw FormatException( + Never _throw(String property, String mustBe) => throw FormatException( '"packages[].$property" in .dart_tool/package_config.json $mustBe'); final name = root['name']; @@ -203,7 +197,7 @@ _throw('name', 'must be a string'); } - Uri rootUri; + final Uri rootUri; final rootUriRaw = root['rootUri']; if (rootUriRaw is! String) { _throw('rootUri', 'must be a string'); @@ -214,7 +208,7 @@ _throw('rootUri', 'must be a URI'); } - Uri packageUri; + Uri? packageUri; var packageUriRaw = root['packageUri']; if (packageUriRaw != null) { if (packageUriRaw is! String) { @@ -230,7 +224,7 @@ } } - LanguageVersion languageVersion; + LanguageVersion? languageVersion; final languageVersionRaw = root['languageVersion']; if (languageVersionRaw != null) { if (languageVersionRaw is! String) { @@ -252,7 +246,7 @@ } /// Convert to JSON structure. - Map<String, Object> toJson() => { + Map<String, Object?> toJson() => { 'name': name, 'rootUri': rootUri.toString(), if (packageUri != null) 'packageUri': packageUri?.toString(),
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart index 5a3d3c0..b825cb0 100644 --- a/lib/src/pubspec.dart +++ b/lib/src/pubspec.dart
@@ -19,17 +19,12 @@ import 'language_version.dart'; import 'log.dart'; import 'package_name.dart'; +import 'pubspec_parse.dart'; import 'sdk.dart'; import 'source_registry.dart'; import 'utils.dart'; -/// A regular expression matching allowed package names. -/// -/// This allows dot-separated valid Dart identifiers. The dots are there for -/// compatibility with Google's internal Dart packages, but they may not be used -/// when publishing a package to pub.dartlang.org. -final _packageName = - RegExp('^${identifierRegExp.pattern}(\\.${identifierRegExp.pattern})*\$'); +export 'pubspec_parse.dart' hide PubspecBase; /// The default SDK upper bound constraint for packages that don't declare one. /// @@ -72,7 +67,7 @@ /// accessed. This allows a partially-invalid pubspec to be used if only the /// valid portions are relevant. To get a list of all errors in the pubspec, use /// [allErrors]. -class Pubspec { +class Pubspec extends PubspecBase { // If a new lazily-initialized field is added to this class and the // initialization can throw a [PubspecException], that error should also be // exposed through [allErrors]. @@ -90,76 +85,6 @@ /// is unknown. Uri get _location => fields.span.sourceUrl; - /// All pubspec fields. - /// - /// This includes the fields from which other properties are derived. - final YamlMap fields; - - /// Whether or not to apply the [_defaultUpperBoundsSdkConstraint] to this - /// pubspec. - final bool _includeDefaultSdkConstraint; - - /// Whether or not the SDK version was overridden from <2.0.0 to - /// <2.0.0-dev.infinity. - bool get dartSdkWasOverridden => _dartSdkWasOverridden; - bool _dartSdkWasOverridden = false; - - /// The package's name. - String get name { - if (_name != null) return _name; - - var name = fields['name']; - if (name == null) { - throw PubspecException('Missing the required "name" field.', fields.span); - } else if (name is! String) { - throw PubspecException( - '"name" field must be a string.', fields.nodes['name'].span); - } else if (!_packageName.hasMatch(name)) { - throw PubspecException('"name" field must be a valid Dart identifier.', - fields.nodes['name'].span); - } else if (reservedWords.contains(name)) { - throw PubspecException('"name" field may not be a Dart reserved word.', - fields.nodes['name'].span); - } - - _name = name; - return _name; - } - - String _name; - - /// The package's version. - Version get version { - if (_version != null) return _version; - - var version = fields['version']; - if (version == null) { - _version = Version.none; - return _version; - } - - var span = fields.nodes['version'].span; - if (version is num) { - var fixed = '$version.0'; - if (version is int) { - fixed = '$fixed.0'; - } - _error( - '"version" field must have three numeric components: major, ' - 'minor, and patch. Instead of "$version", consider "$fixed".', - span); - } - if (version is! String) { - _error('"version" field must be a string.', span); - } - - _version = _wrapFormatException( - 'version number', span, () => Version.parse(version)); - return _version; - } - - Version _version; - /// The additional packages this package depends on. Map<String, PackageRange> get dependencies { if (_dependencies != null) return _dependencies; @@ -251,6 +176,15 @@ Map<String, VersionConstraint> _sdkConstraints; + /// Whether or not to apply the [_defaultUpperBoundsSdkConstraint] to this + /// pubspec. + final bool _includeDefaultSdkConstraint; + + /// Whether or not the SDK version was overridden from <2.0.0 to + /// <2.0.0-dev.infinity. + bool get dartSdkWasOverridden => _dartSdkWasOverridden; + bool _dartSdkWasOverridden = false; + /// The original Dart SDK constraint as written in the pubspec. /// /// If [dartSdkWasOverridden] is `false`, this will be identical to @@ -353,137 +287,6 @@ return constraints; } - /// The URL of the server that the package should default to being published - /// to, "none" if the package should not be published, or `null` if it should - /// be published to the default server. - /// - /// If this does return a URL string, it will be a valid parseable URL. - String get publishTo { - if (_parsedPublishTo) return _publishTo; - - var publishTo = fields['publish_to']; - if (publishTo != null) { - var span = fields.nodes['publish_to'].span; - - if (publishTo is! String) { - _error('"publish_to" field must be a string.', span); - } - - // It must be "none" or a valid URL. - if (publishTo != 'none') { - _wrapFormatException('"publish_to" field', span, () { - var url = Uri.parse(publishTo); - if (url.scheme.isEmpty) { - throw FormatException('must be an absolute URL.'); - } - }); - } - } - - _parsedPublishTo = true; - _publishTo = publishTo; - return _publishTo; - } - - bool _parsedPublishTo = false; - String _publishTo; - - /// The list of patterns covering _false-positive secrets_ in the package. - /// - /// This is a list of git-ignore style patterns for files that should be - /// ignored when trying to detect possible leaks of secrets during - /// package publication. - List<String> get falseSecrets { - if (_falseSecrets == null) { - final falseSecrets = <String>[]; - - // Throws a [PubspecException] - void _falseSecretsError(SourceSpan span) => _error( - '"false_secrets" field must be a list of git-ignore style patterns', - span, - ); - - final falseSecretsNode = fields.nodes['false_secrets']; - if (falseSecretsNode != null) { - if (falseSecretsNode is YamlList) { - for (final node in falseSecretsNode.nodes) { - final value = node.value; - if (value is! String) { - _falseSecretsError(node.span); - } - falseSecrets.add(value); - } - } else { - _falseSecretsError(falseSecretsNode.span); - } - } - - _falseSecrets = List.unmodifiable(falseSecrets); - } - return _falseSecrets; - } - - List<String> _falseSecrets; - - /// The executables that should be placed on the user's PATH when this - /// package is globally activated. - /// - /// It is a map of strings to string. Each key is the name of the command - /// that will be placed on the user's PATH. The value is the name of the - /// .dart script (without extension) in the package's `bin` directory that - /// should be run for that command. Both key and value must be "simple" - /// strings: alphanumerics, underscores and hypens only. If a value is - /// omitted, it is inferred to use the same name as the key. - Map<String, String> get executables { - if (_executables != null) return _executables; - - _executables = {}; - var yaml = fields['executables']; - if (yaml == null) return _executables; - - if (yaml is! Map) { - _error('"executables" field must be a map.', - fields.nodes['executables'].span); - } - - yaml.nodes.forEach((key, value) { - if (key.value is! String) { - _error('"executables" keys must be strings.', key.span); - } - - final keyPattern = RegExp(r'^[a-zA-Z0-9_-]+$'); - if (!keyPattern.hasMatch(key.value)) { - _error( - '"executables" keys may only contain letters, ' - 'numbers, hyphens and underscores.', - key.span); - } - - if (value.value == null) { - value = key; - } else if (value.value is! String) { - _error('"executables" values must be strings or null.', value.span); - } - - final valuePattern = RegExp(r'[/\\]'); - if (valuePattern.hasMatch(value.value)) { - _error('"executables" values may not contain path separators.', - value.span); - } - - _executables[key.value] = value.value; - }); - - return _executables; - } - - Map<String, String> _executables; - - /// Whether the package is private and cannot be published. - /// - /// This is specified in the pubspec by setting "publish_to" to "none". - bool get isPrivate => publishTo == 'none'; - /// Whether or not the pubspec has no contents. bool get isEmpty => name == null && version == Version.none && dependencies.isEmpty; @@ -513,7 +316,7 @@ expectedName: expectedName, location: pubspecUri); } - Pubspec(this._name, + Pubspec(String name, {Version version, Iterable<PackageRange> dependencies, Iterable<PackageRange> devDependencies, @@ -521,8 +324,7 @@ Map fields, SourceRegistry sources, Map<String, VersionConstraint> sdkConstraints}) - : _version = version, - _dependencies = dependencies == null + : _dependencies = dependencies == null ? null : Map.fromIterable(dependencies, key: (range) => range.name), _devDependencies = devDependencies == null @@ -534,18 +336,23 @@ _sdkConstraints = sdkConstraints ?? UnmodifiableMapView({'dart': VersionConstraint.any}), _includeDefaultSdkConstraint = false, - fields = fields == null ? YamlMap() : YamlMap.wrap(fields), - _sources = sources; + _sources = sources, + super( + fields == null ? YamlMap() : YamlMap.wrap(fields), + name: name, + version: version, + ); Pubspec.empty() : _sources = null, - _name = null, - _version = Version.none, _dependencies = {}, _devDependencies = {}, _sdkConstraints = {'dart': VersionConstraint.any}, _includeDefaultSdkConstraint = false, - fields = YamlMap(); + super( + YamlMap(), + version: Version.none, + ); /// Returns a Pubspec object for an already-parsed map representing its /// contents. @@ -556,10 +363,10 @@ /// [location] is the location from which this pubspec was loaded. Pubspec.fromMap(Map fields, this._sources, {String expectedName, Uri location}) - : fields = fields is YamlMap + : _includeDefaultSdkConstraint = true, + super(fields is YamlMap ? fields - : YamlMap.wrap(fields, sourceUrl: location), - _includeDefaultSdkConstraint = true { + : YamlMap.wrap(fields, sourceUrl: location)) { // If [expectedName] is passed, ensure that the actual 'name' field exists // and matches the expectation. if (expectedName == null) return; @@ -778,7 +585,7 @@ var name = node.value; if (name is! String) { _error('A feature name must be a string.', node.span); - } else if (!_packageName.hasMatch(name)) { + } else if (!packageNameRegExp.hasMatch(name)) { _error('A feature name must be a valid Dart identifier.', node.span); } @@ -840,14 +647,6 @@ } } -/// An exception thrown when parsing a pubspec. -/// -/// These exceptions are often thrown lazily while accessing pubspec properties. -class PubspecException extends SourceSpanFormatException - implements ApplicationException { - PubspecException(String message, SourceSpan span) : super(message, span); -} - /// Returns whether [uri] is a file URI. /// /// This is slightly more complicated than just checking if the scheme is
diff --git a/lib/src/pubspec_parse.dart b/lib/src/pubspec_parse.dart new file mode 100644 index 0000000..dccc438 --- /dev/null +++ b/lib/src/pubspec_parse.dart
@@ -0,0 +1,263 @@ +// 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:meta/meta.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +import 'exceptions.dart' show ApplicationException; +import 'utils.dart' show identifierRegExp, reservedWords; + +/// A regular expression matching allowed package names. +/// +/// This allows dot-separated valid Dart identifiers. The dots are there for +/// compatibility with Google's internal Dart packages, but they may not be used +/// when publishing a package to pub.dartlang.org. +final packageNameRegExp = + RegExp('^${identifierRegExp.pattern}(\\.${identifierRegExp.pattern})*\$'); + +/// Helper class for pubspec parsing to: +/// - extract the fields and methods that are reusable outside of `pub` client, and +/// - help null-safety migration a bit. +/// +/// This class should be eventually extracted to a separate library, or re-merged with `Pubspec`. +abstract class PubspecBase { + /// All pubspec fields. + /// + /// This includes the fields from which other properties are derived. + final YamlMap fields; + + PubspecBase( + this.fields, { + String? name, + Version? version, + }) : _name = name, + _version = version; + + /// The package's name. + String get name { + if (_name != null) return _name!; + + final name = fields['name']; + if (name == null) { + throw PubspecException('Missing the required "name" field.', fields.span); + } else if (name is! String) { + throw PubspecException( + '"name" field must be a string.', fields.nodes['name']?.span); + } else if (!packageNameRegExp.hasMatch(name)) { + throw PubspecException('"name" field must be a valid Dart identifier.', + fields.nodes['name']?.span); + } else if (reservedWords.contains(name)) { + throw PubspecException('"name" field may not be a Dart reserved word.', + fields.nodes['name']?.span); + } + + _name = name; + return _name!; + } + + String? _name; + + /// The package's version. + Version get version { + if (_version != null) return _version!; + + final version = fields['version']; + if (version == null) { + _version = Version.none; + return _version!; + } + + final span = fields.nodes['version']?.span; + if (version is num) { + var fixed = '$version.0'; + if (version is int) { + fixed = '$fixed.0'; + } + _error( + '"version" field must have three numeric components: major, ' + 'minor, and patch. Instead of "$version", consider "$fixed".', + span); + } + if (version is! String) { + _error('"version" field must be a string.', span); + } + + _version = _wrapFormatException( + 'version number', span, () => Version.parse(version)); + return _version!; + } + + Version? _version; + + /// The URL of the server that the package should default to being published + /// to, "none" if the package should not be published, or `null` if it should + /// be published to the default server. + /// + /// If this does return a URL string, it will be a valid parseable URL. + String? get publishTo { + if (_parsedPublishTo) return _publishTo; + + final publishTo = fields['publish_to']; + if (publishTo != null) { + final span = fields.nodes['publish_to']?.span; + + if (publishTo is! String) { + _error('"publish_to" field must be a string.', span); + } + + // It must be "none" or a valid URL. + if (publishTo != 'none') { + _wrapFormatException('"publish_to" field', span, () { + final url = Uri.parse(publishTo); + if (url.scheme.isEmpty) { + throw FormatException('must be an absolute URL.'); + } + }); + } + } + + _parsedPublishTo = true; + _publishTo = publishTo; + return _publishTo; + } + + bool _parsedPublishTo = false; + String? _publishTo; + + /// The list of patterns covering _false-positive secrets_ in the package. + /// + /// This is a list of git-ignore style patterns for files that should be + /// ignored when trying to detect possible leaks of secrets during + /// package publication. + List<String> get falseSecrets { + if (_falseSecrets == null) { + final falseSecrets = <String>[]; + + // Throws a [PubspecException] + void _falseSecretsError(SourceSpan span) => _error( + '"false_secrets" field must be a list of git-ignore style patterns', + span, + ); + + final falseSecretsNode = fields.nodes['false_secrets']; + if (falseSecretsNode != null) { + if (falseSecretsNode is YamlList) { + for (final node in falseSecretsNode.nodes) { + final value = node.value; + if (value is! String) { + _falseSecretsError(node.span); + } + falseSecrets.add(value); + } + } else { + _falseSecretsError(falseSecretsNode.span); + } + } + + _falseSecrets = List.unmodifiable(falseSecrets); + } + return _falseSecrets!; + } + + List<String>? _falseSecrets; + + /// The executables that should be placed on the user's PATH when this + /// package is globally activated. + /// + /// It is a map of strings to string. Each key is the name of the command + /// that will be placed on the user's PATH. The value is the name of the + /// .dart script (without extension) in the package's `bin` directory that + /// should be run for that command. Both key and value must be "simple" + /// strings: alphanumerics, underscores and hypens only. If a value is + /// omitted, it is inferred to use the same name as the key. + Map<String, String> get executables { + if (_executables != null) return _executables!; + + _executables = {}; + var yaml = fields['executables']; + if (yaml == null) return _executables!; + + if (yaml is! Map) { + _error('"executables" field must be a map.', + fields.nodes['executables']?.span); + } + + yaml.nodes.forEach((key, value) { + if (key.value is! String) { + _error('"executables" keys must be strings.', key.span); + } + + final keyPattern = RegExp(r'^[a-zA-Z0-9_-]+$'); + if (!keyPattern.hasMatch(key.value)) { + _error( + '"executables" keys may only contain letters, ' + 'numbers, hyphens and underscores.', + key.span); + } + + if (value.value == null) { + value = key; + } else if (value.value is! String) { + _error('"executables" values must be strings or null.', value.span); + } + + final valuePattern = RegExp(r'[/\\]'); + if (valuePattern.hasMatch(value.value)) { + _error('"executables" values may not contain path separators.', + value.span); + } + + _executables![key.value] = value.value; + }); + + return _executables!; + } + + Map<String, String>? _executables; + + /// Whether the package is private and cannot be published. + /// + /// This is specified in the pubspec by setting "publish_to" to "none". + bool get isPrivate => publishTo == 'none'; + + /// Runs [fn] and wraps any [FormatException] it throws in a + /// [PubspecException]. + /// + /// [description] should be a noun phrase that describes whatever's being + /// parsed or processed by [fn]. [span] should be the location of whatever's + /// being processed within the pubspec. + /// + /// If [targetPackage] is provided, the value is used to describe the + /// dependency that caused the problem. + T _wrapFormatException<T>( + String description, SourceSpan? span, T Function() fn, + {String? targetPackage}) { + try { + return fn(); + } on FormatException catch (e) { + var msg = 'Invalid $description'; + if (targetPackage != null) { + msg = '$msg in the "$name" pubspec on the "$targetPackage" dependency'; + } + msg = '$msg: ${e.message}'; + throw PubspecException(msg, span); + } + } + + /// Throws a [PubspecException] with the given message. + @alwaysThrows + void _error(String message, SourceSpan? span) { + throw PubspecException(message, span); + } +} + +/// An exception thrown when parsing a pubspec. +/// +/// These exceptions are often thrown lazily while accessing pubspec properties. +class PubspecException extends SourceSpanFormatException + implements ApplicationException { + PubspecException(String message, SourceSpan? span) : super(message, span); +}
diff --git a/lib/src/rate_limited_scheduler.dart b/lib/src/rate_limited_scheduler.dart index 6e62310..daeb76f 100644 --- a/lib/src/rate_limited_scheduler.dart +++ b/lib/src/rate_limited_scheduler.dart
@@ -2,12 +2,9 @@ // 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=2.10 - import 'dart:async'; import 'dart:collection'; -import 'package:meta/meta.dart'; import 'package:pedantic/pedantic.dart'; import 'package:pool/pool.dart'; @@ -65,7 +62,7 @@ final Set<J> _started = {}; RateLimitedScheduler(Future<V> Function(J) runJob, - {@required int maxConcurrentOperations}) + {required int maxConcurrentOperations}) : _runJob = runJob, _pool = Pool(maxConcurrentOperations); @@ -77,7 +74,7 @@ return; } final task = _queue.removeFirst(); - final completer = _cache[task.jobId]; + final completer = _cache[task.jobId]!; if (!_started.add(task.jobId)) { return; @@ -140,7 +137,7 @@ /// Returns the result of running [jobId] if that is already done. /// Otherwise returns `null`. - V peek(J jobId) => _results[jobId]; + V? peek(J jobId) => _results[jobId]; } class _Task<J> {
diff --git a/lib/src/solver/reformat_ranges.dart b/lib/src/solver/reformat_ranges.dart index bdb64b8..d40bd6f 100644 --- a/lib/src/solver/reformat_ranges.dart +++ b/lib/src/solver/reformat_ranges.dart
@@ -4,6 +4,7 @@ // @dart=2.10 +import 'package:meta/meta.dart'; import 'package:pub_semver/pub_semver.dart'; import '../package_name.dart'; @@ -48,7 +49,7 @@ var range = term.package.constraint as VersionRange; var min = _reformatMin(versions, range); - var tuple = _reformatMax(versions, range); + var tuple = reformatMax(versions, range); var max = tuple?.first; var includeMax = tuple?.last; @@ -84,10 +85,16 @@ /// Returns the new maximum version to use for [range] and whether that maximum /// is inclusive, or `null` if it doesn't need to be reformatted. -Pair<Version, bool> _reformatMax(List<PackageId> versions, VersionRange range) { +@visibleForTesting +Pair<Version, bool> reformatMax(List<PackageId> versions, VersionRange range) { + // This corresponds to the logic in the constructor of [VersionRange] with + // `alwaysIncludeMaxPreRelease = false` for discovering when a max-bound + // should not include prereleases. + if (range.max == null) return null; if (range.includeMax) return null; if (range.max.isPreRelease) return null; + if (range.max.build.isNotEmpty) return null; if (range.min != null && range.min.isPreRelease && equalsIgnoringPreRelease(range.min, range.max)) {
diff --git a/lib/src/validator/gitignore.dart b/lib/src/validator/gitignore.dart index df01214..0986fed 100644 --- a/lib/src/validator/gitignore.dart +++ b/lib/src/validator/gitignore.dart
@@ -31,18 +31,25 @@ '--exclude-standard', '--recurse-submodules' ], workingDir: entrypoint.root.dir); + final root = git.repoRoot(entrypoint.root.dir) ?? entrypoint.root.dir; + var beneath = p.posix.joinAll( + p.split(p.normalize(p.relative(entrypoint.root.dir, from: root)))); + if (beneath == './') { + beneath = ''; + } String resolve(String path) { if (Platform.isWindows) { - return p.joinAll([entrypoint.root.dir, ...p.posix.split(path)]); + return p.joinAll([root, ...p.posix.split(path)]); } - return p.join(entrypoint.root.dir, path); + return p.join(root, path); } final unignoredByGitignore = Ignore.listFiles( + beneath: beneath, listDir: (dir) { var contents = Directory(resolve(dir)).listSync(); - return contents.map((entity) => p.posix.joinAll( - p.split(p.relative(entity.path, from: entrypoint.root.dir)))); + return contents.map((entity) => + p.posix.joinAll(p.split(p.relative(entity.path, from: root)))); }, ignoreForDir: (dir) { final gitIgnore = resolve('$dir/.gitignore'); @@ -52,8 +59,12 @@ return rules.isEmpty ? null : Ignore(rules); }, isDir: (dir) => dirExists(resolve(dir)), - ).toSet(); - + ).map((file) { + final relative = p.relative(resolve(file), from: entrypoint.root.dir); + return Platform.isWindows + ? p.posix.joinAll(p.split(relative)) + : relative; + }).toSet(); final ignoredFilesCheckedIn = checkedIntoGit .where((file) => !unignoredByGitignore.contains(file)) .toList();
diff --git a/test/package_list_files_test.dart b/test/package_list_files_test.dart index 7891270..921175d 100644 --- a/test/package_list_files_test.dart +++ b/test/package_list_files_test.dart
@@ -254,6 +254,23 @@ }); }); + test("Don't ignore packages/ before the package root", () async { + await d.dir(appPath, [ + d.dir('packages', [ + d.dir('app', [ + d.appPubspec(), + d.dir('packages', [d.file('a.txt')]), + ]), + ]), + ]).create(); + + createEntrypoint(p.join(appPath, 'packages', 'app')); + + expect(entrypoint.root.listFiles(), { + p.join(root, 'pubspec.yaml'), + }); + }); + group('with a submodule', () { setUp(() async { await d.git('submodule', [
diff --git a/test/reformat_ranges_test.dart b/test/reformat_ranges_test.dart new file mode 100644 index 0000000..4af9cf7 --- /dev/null +++ b/test/reformat_ranges_test.dart
@@ -0,0 +1,57 @@ +// 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. + +// @dart=2.10 + +import 'package:pub/src/package_name.dart'; +import 'package:pub/src/solver/reformat_ranges.dart'; +import 'package:pub/src/utils.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +void main() { + test('reformatMax when max has a build identifier', () { + expect( + reformatMax( + [PackageId('abc', null, Version.parse('1.2.3'), null)], + VersionRange( + min: Version.parse('0.2.4'), + max: Version.parse('1.2.4'), + alwaysIncludeMaxPreRelease: true, + ), + ), + equals( + Pair( + Version.parse('1.2.4-0'), + false, + ), + ), + ); + expect( + reformatMax( + [PackageId('abc', null, Version.parse('1.2.4-3'), null)], + VersionRange( + min: Version.parse('0.2.4'), + max: Version.parse('1.2.4'), + alwaysIncludeMaxPreRelease: true, + ), + ), + equals( + Pair( + Version.parse('1.2.4-3'), + true, + ), + ), + ); + expect( + reformatMax( + [], + VersionRange( + max: Version.parse('1.2.4+1'), + alwaysIncludeMaxPreRelease: true, + ), + ), + equals(null)); + }); +}
diff --git a/test/validator/gitignore_test.dart b/test/validator/gitignore_test.dart index d6140f5..06d3be4 100644 --- a/test/validator/gitignore_test.dart +++ b/test/validator/gitignore_test.dart
@@ -4,18 +4,23 @@ // @dart=2.10 +import 'package:path/path.dart' as p; import 'package:pub/src/exit_codes.dart' as exit_codes; import 'package:test/test.dart'; import '../descriptor.dart' as d; import '../test_pub.dart'; -Future<void> expectValidation(error, int exitCode) async { +Future<void> expectValidation( + error, + int exitCode, { + String workingDirectory, +}) async { await runPub( error: error, args: ['publish', '--dry-run'], environment: {'_PUB_TEST_SDK_VERSION': '2.12.0'}, - workingDirectory: d.path(appPath), + workingDirectory: workingDirectory ?? d.path(appPath), exitCode: exitCode, ); } @@ -46,4 +51,37 @@ ]), exit_codes.DATA); }); + + test('Should also consider gitignores from above the package root', () async { + await d.git('reporoot', [ + d.dir( + 'myapp', + [ + d.file('foo.txt'), + ...d.validPackage.contents, + ], + ), + ]).create(); + final packageRoot = p.join(d.sandbox, 'reporoot', 'myapp'); + await pubGet( + environment: {'_PUB_TEST_SDK_VERSION': '1.12.0'}, + workingDirectory: packageRoot); + + await expectValidation(contains('Package has 0 warnings.'), 0, + workingDirectory: packageRoot); + + await d.dir('reporoot', [ + d.file('.gitignore', '*.txt'), + ]).create(); + + await expectValidation( + allOf([ + contains('Package has 1 warning.'), + contains('foo.txt'), + contains( + 'Consider adjusting your `.gitignore` files to not ignore those files'), + ]), + exit_codes.DATA, + workingDirectory: packageRoot); + }); }
diff --git a/test/version_solver_test.dart b/test/version_solver_test.dart index e2136c5..caca301 100644 --- a/test/version_solver_test.dart +++ b/test/version_solver_test.dart
@@ -31,6 +31,8 @@ group('override', override); group('downgrade', downgrade); group('features', features, skip: true); + + group('regressions', regressions); } void basicGraph() { @@ -3014,3 +3016,22 @@ expect(ids, isEmpty, reason: 'Expected no additional packages.'); } + +void regressions() { + test('reformatRanges with a build', () async { + await servePackages((b) { + b.serve('integration_test', '1.0.1', + deps: {'vm_service': '>= 4.2.0 <6.0.0'}); + b.serve('integration_test', '1.0.2+2', + deps: {'vm_service': '>= 4.2.0 <7.0.0'}); + + b.serve('vm_service', '7.3.0'); + }); + await d.appDir({'integration_test': '^1.0.2'}).create(); + await expectResolves( + error: contains( + 'Because no versions of integration_test match >=1.0.2 <1.0.2+2', + ), + ); + }); +}
diff --git a/tool/test.dart b/tool/test.dart index 2f4769d..e7877a2 100755 --- a/tool/test.dart +++ b/tool/test.dart
@@ -3,8 +3,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. -// @dart=2.10 - /// Test wrapper script. /// Many of the integration tests runs the `pub` command, this is slow if every /// invocation requires the dart compiler to load all the sources. This script @@ -19,7 +17,7 @@ import 'package:pub/src/exceptions.dart'; Future<void> main(List<String> args) async { - Process testProcess; + Process? testProcess; final sub = ProcessSignal.sigint.watch().listen((signal) { testProcess?.kill(signal); });