diff --git a/bin/dependency_services.dart b/bin/dependency_services.dart index f117afd..90e528a 100644 --- a/bin/dependency_services.dart +++ b/bin/dependency_services.dart
@@ -20,7 +20,7 @@ class _DependencyServicesCommandRunner extends CommandRunner<int> implements PubTopLevel { @override - String? get directory => argResults['directory']; + String get directory => argResults['directory']; @override bool get captureStackChains => argResults['verbose'];
diff --git a/lib/src/command.dart b/lib/src/command.dart index e86dd50..31f0b17 100644 --- a/lib/src/command.dart +++ b/lib/src/command.dart
@@ -55,11 +55,12 @@ return a; } - String get directory => - (argResults.options.contains('directory') - ? argResults['directory'] - : null) ?? - _pubTopLevel.directory; + String get directory { + return (argResults.options.contains('directory') + ? argResults['directory'] + : null) ?? + _pubTopLevel.directory; + } late final SystemCache cache = SystemCache(isOffline: isOffline); @@ -194,7 +195,6 @@ return exit_codes.SUCCESS; } catch (error, chain) { log.exception(error, chain); - if (_pubTopLevel.trace) { log.dumpTranscriptToStdErr(); } else if (!isUserFacingException(error)) { @@ -351,7 +351,7 @@ } /// The directory containing the pubspec.yaml of the project to work on. - String? get directory; + String get directory; /// The argResults from the level of parsing of the 'pub' command. ArgResults get argResults;
diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart index e45a735..e516168 100644 --- a/lib/src/command/lish.dart +++ b/lib/src/command/lish.dart
@@ -155,9 +155,9 @@ Future<void> _publish(List<int> packageBytes) async { try { final officialPubServers = { - 'https://pub.dev', - // [validateAndNormalizeHostedUrl] normalizes https://pub.dartlang.org - // to https://pub.dev, so we don't need to do allow that here. + 'https://pub.dartlang.org', + // [validateAndNormalizeHostedUrl] normalizes https://pub.dev + // to https://pub.dartlang.org, so we don't need to do allow that here. // Pub uses oauth2 credentials only for authenticating official pub // servers for security purposes (to not expose pub.dev access token to
diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart index 92d4cf8..150de16 100644 --- a/lib/src/command_runner.dart +++ b/lib/src/command_runner.dart
@@ -45,7 +45,7 @@ class PubCommandRunner extends CommandRunner<int> implements PubTopLevel { @override - String? get directory => argResults['directory']; + String get directory => argResults['directory']; @override bool get captureStackChains {
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart index b536cca..2683cdf 100644 --- a/lib/src/entrypoint.dart +++ b/lib/src/entrypoint.dart
@@ -12,6 +12,7 @@ import 'package:path/path.dart' as p; import 'package:pool/pool.dart'; import 'package:pub_semver/pub_semver.dart'; +import 'package:source_span/source_span.dart'; import 'package:yaml/yaml.dart'; import 'command_runner.dart'; @@ -19,7 +20,6 @@ import 'exceptions.dart'; import 'executable.dart'; import 'io.dart'; -import 'language_version.dart'; import 'lock_file.dart'; import 'log.dart' as log; import 'package.dart'; @@ -101,7 +101,17 @@ if (!fileExists(lockFilePath)) { return _lockFile = LockFile.empty(); } else { - return _lockFile = LockFile.load(lockFilePath, cache.sources); + try { + return _lockFile = LockFile.load(lockFilePath, cache.sources); + } on SourceSpanException catch (e) { + throw SourceSpanApplicationException( + e.message, + e.span, + explanation: 'Failed parsing lock file:', + hint: + 'Consider deleting the file and running `$topLevelProgram pub get` to recreate it.', + ); + } } } @@ -270,7 +280,8 @@ await lockFile.packageConfigFile( cache, entrypoint: entrypointName, - entrypointSdkConstraint: root.pubspec.sdkConstraints[sdk.identifier], + entrypointSdkConstraint: + root.pubspec.sdkConstraints[sdk.identifier]?.effectiveConstraint, relativeFrom: isGlobal ? null : root.dir, ), ); @@ -805,9 +816,7 @@ try { // Load `pubspec.yaml` and extract language version to compare with the // language version from `package_config.json`. - final languageVersion = LanguageVersion.fromSdkConstraint( - cache.load(id).pubspec.sdkConstraints[sdk.identifier], - ); + final languageVersion = cache.load(id).pubspec.languageVersion; if (pkg.languageVersion != languageVersion) { final relativePubspecPath = p.join( cache.getDirectory(id, relativeFrom: '.'), @@ -863,7 +872,7 @@ /// /// We don't allow unknown sdks. void _checkSdkConstraint(Pubspec pubspec) { - final dartSdkConstraint = pubspec.sdkConstraints['dart']; + final dartSdkConstraint = pubspec.dartSdkConstraint.effectiveConstraint; if (dartSdkConstraint is! VersionRange || dartSdkConstraint.min == null) { // Suggest version range '>=2.10.0 <3.0.0', we avoid using: // [CompatibleWithVersionRange] because some pub versions don't support @@ -898,7 +907,7 @@ final keyNode = environment.nodes.entries .firstWhere((e) => (e.key as YamlNode).value == sdk) .key as YamlNode; - throw PubspecException(''' + throw SourceSpanApplicationException(''' $pubspecPath refers to an unknown sdk '$sdk'. Did you mean to add it as a dependency?
diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart index ec9f876..39e9c51 100644 --- a/lib/src/exceptions.dart +++ b/lib/src/exceptions.dart
@@ -7,6 +7,7 @@ import 'package:args/command_runner.dart'; import 'package:http/http.dart' as http; +import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; import 'package:yaml/yaml.dart'; @@ -127,3 +128,34 @@ error is YamlException || error is UsageException; } + +/// An exception thrown when parsing a `pubspec.yaml` or a `pubspec.lock`. +/// +/// These exceptions are often thrown lazily while accessing pubspec properties. +/// +/// By being an [ApplicationException] this will not trigger a stack-trace on +/// normal operations. +/// +/// Works as a [SourceSpanFormatException], but can contain more context: +/// An optional [explanation] that explains the operation that failed. +/// An optional [hint] that gives suggestions how to proceed. +class SourceSpanApplicationException extends SourceSpanFormatException + implements ApplicationException { + final String? explanation; + final String? hint; + + SourceSpanApplicationException(String message, SourceSpan? span, + {this.hint, this.explanation}) + : super(message, span); + + @override + String toString({color}) { + return [ + if (explanation != null) explanation, + span == null + ? message + : 'Error on ${span?.message(message, color: color)}', + if (hint != null) hint, + ].join('\n\n'); + } +}
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart index bb97023..9d79a16 100644 --- a/lib/src/global_packages.dart +++ b/lib/src/global_packages.dart
@@ -362,7 +362,9 @@ dataError('${log.bold(name)} ${entrypoint.root.version} requires ' 'unknown SDK "$name".'); } else if (sdkName == 'dart') { - if (constraint.allows((sdk as DartSdk).version)) return; + if (constraint.effectiveConstraint.allows((sdk as DartSdk).version)) { + return; + } dataError("${log.bold(name)} ${entrypoint.root.version} doesn't " 'support Dart ${sdk.version}.'); } else { @@ -730,35 +732,18 @@ } } - // If the script was built to a snapshot, just try to invoke that - // directly and skip pub global run entirely. - String invocation; late String binstub; + // Batch files behave in funky ways if they are modified while updating. + // To ensure that the byte-offsets of everything stays the same even if the + // snapshot filename changes we insert some padding in lines containing the + // snapshot. + // 260 is the maximal short path length on Windows. Hopefully that is + // enough. + final padding = ' ' * (260 - snapshot.length); + // We need an absolute path since relative ones won't be relative to the + // right directory when the user runs this. + snapshot = p.absolute(snapshot); if (Platform.isWindows) { - if (fileExists(snapshot)) { - // We expect absolute paths from the precompiler since relative ones - // won't be relative to the right directory when the user runs this. - assert(p.isAbsolute(snapshot)); - invocation = ''' -if exist "$snapshot" ( - call dart "$snapshot" %* - rem The VM exits with code 253 if the snapshot version is out-of-date. - rem If it is, we need to delete it and run "pub global" manually. - if not errorlevel 253 ( - goto error - ) - dart pub global run ${package.name}:$script %* -) else ( - dart pub global run ${package.name}:$script %* -) -goto eof -:error -exit /b %errorlevel% -:eof -'''; - } else { - invocation = 'dart pub global run ${package.name}:$script %*'; - } binstub = ''' @echo off rem This file was created by pub v${sdk.version}. @@ -766,14 +751,30 @@ rem Version: ${package.version} rem Executable: $executable rem Script: $script -$invocation +if exist "$snapshot" $padding( + call dart "$snapshot" $padding%* + rem The VM exits with code 253 if the snapshot version is out-of-date. + rem If it is, we need to delete it and run "pub global" manually. + if not errorlevel 253 ( + goto error + ) + call dart pub global run ${package.name}:$script %* +) else ( + call dart pub global run ${package.name}:$script %* +) +goto eof +:error +exit /b %errorlevel% +:eof '''; } else { - if (fileExists(snapshot)) { - // We expect absolute paths from the precompiler since relative ones - // won't be relative to the right directory when the user runs this. - assert(p.isAbsolute(snapshot)); - invocation = ''' + binstub = ''' +#!/usr/bin/env sh +# This file was created by pub v${sdk.version}. +# Package: ${package.name} +# Version: ${package.version} +# Executable: $executable +# Script: $script if [ -f $snapshot ]; then dart "$snapshot" "\$@" # The VM exits with code 253 if the snapshot version is out-of-date. @@ -787,18 +788,6 @@ dart pub global run ${package.name}:$script "\$@" fi '''; - } else { - invocation = 'dart pub global run ${package.name}:$script "\$@"'; - } - binstub = ''' -#!/usr/bin/env sh -# This file was created by pub v${sdk.version}. -# Package: ${package.name} -# Version: ${package.version} -# Executable: $executable -# Script: $script -$invocation -'''; } // Write the binstub to a temporary location, make it executable and move
diff --git a/lib/src/lock_file.dart b/lib/src/lock_file.dart index 46be2bf..6c66224 100644 --- a/lib/src/lock_file.dart +++ b/lib/src/lock_file.dart
@@ -98,50 +98,54 @@ Uri? sourceUrl; if (filePath != null) sourceUrl = p.toUri(filePath); - var parsed = loadYamlNode(contents, sourceUrl: sourceUrl); + final parsed = _parseNode<YamlMap>( + loadYamlNode(contents, sourceUrl: sourceUrl), + 'YAML mapping', + ); - _validate(parsed is Map, 'The lockfile must be a YAML mapping.', parsed); - var parsedMap = parsed as YamlMap; - - var sdkConstraints = <String, VersionConstraint>{}; - var sdkNode = parsedMap.nodes['sdk']; + final sdkConstraints = <String, VersionConstraint>{}; + final sdkNode = + _getEntry<YamlScalar?>(parsed, 'sdk', 'string', required: false); if (sdkNode != null) { // Lockfiles produced by pub versions from 1.14.0 through 1.18.0 included // a top-level "sdk" field which encoded the unified constraint on the // Dart SDK. They had no way of specifying constraints on other SDKs. sdkConstraints['dart'] = _parseVersionConstraint(sdkNode); - } else if (parsedMap.containsKey('sdks')) { - var sdksField = parsedMap['sdks']; - _validate(sdksField is Map, 'The "sdks" field must be a mapping.', - parsedMap.nodes['sdks']); - - sdksField.nodes.forEach((name, constraint) { - _validate(name.value is String, 'SDK names must be strings.', name); - sdkConstraints[name.value as String] = - _parseVersionConstraint(constraint); - }); } - var packages = <String, PackageId>{}; - var packageEntries = parsedMap['packages']; - if (packageEntries != null) { - _validate(packageEntries is Map, 'The "packages" field must be a map.', - parsedMap.nodes['packages']); + final sdksField = + _getEntry<YamlMap?>(parsed, 'sdks', 'map', required: false); - packageEntries.forEach((name, spec) { + if (sdksField != null) { + _parseEachEntry<String, YamlScalar>(sdksField, (name, constraint) { + sdkConstraints[name] = _parseVersionConstraint(constraint); + }, 'string', 'string'); + } + + final packages = <String, PackageId>{}; + + final mainDependencies = <String>{}; + final devDependencies = <String>{}; + final overriddenDependencies = <String>{}; + + final packageEntries = + _getEntry<YamlMap?>(parsed, 'packages', 'map', required: false); + + if (packageEntries != null) { + _parseEachEntry<String, YamlMap>(packageEntries, (name, spec) { // Parse the version. - _validate(spec.containsKey('version'), - 'Package $name is missing a version.', spec); - var version = Version.parse(spec['version']); + final versionEntry = _getStringEntry(spec, 'version'); + final version = Version.parse(versionEntry); // Parse the source. - _validate(spec.containsKey('source'), - 'Package $name is missing a source.', spec); - var sourceName = spec['source']; + final sourceName = _getStringEntry(spec, 'source'); - _validate(spec.containsKey('description'), - 'Package $name is missing a description.', spec); - var description = spec['description']; + var descriptionNode = + _getEntry<YamlNode>(spec, 'description', 'description'); + + dynamic description = descriptionNode is YamlScalar + ? descriptionNode.value + : descriptionNode; // Let the source parse the description. var source = sources(sourceName); @@ -150,18 +154,30 @@ id = source.parseId(name, version, description, containingDir: filePath == null ? null : p.dirname(filePath)); } on FormatException catch (ex) { - throw SourceSpanFormatException( - ex.message, spec.nodes['description'].span); + _failAt(ex.message, spec.nodes['description']!); } // Validate the name. - _validate(name == id.name, - "Package name $name doesn't match ${id.name}.", spec); + if (name != id.name) { + _failAt("Package name $name doesn't match ${id.name}.", spec); + } packages[name] = id; - }); + if (spec.containsKey('dependency')) { + final dependencyKind = _getStringEntry(spec, 'dependency'); + switch (dependencyKind) { + case _directMain: + mainDependencies.add(name); + break; + case _directDev: + devDependencies.add(name); + break; + case _directOverridden: + overriddenDependencies.add(name); + } + } + }, 'string', 'map'); } - return LockFile._( packages, sdkConstraints, @@ -170,15 +186,6 @@ const UnmodifiableSetView.empty()); } - /// Asserts that [node] is a version constraint, and parses it. - static VersionConstraint _parseVersionConstraint(YamlNode node) { - _validate(node.value is String, - 'Invalid version constraint: must be a string.', node); - - return _wrapFormatException('version constraint', node.span, - () => VersionConstraint.parse(node.value)); - } - /// Runs [fn] and wraps any [FormatException] it throws in a /// [SourceSpanFormatException]. /// @@ -195,10 +202,68 @@ } } - /// If [condition] is `false` throws a format error with [message] for [node]. - static void _validate(bool condition, String message, YamlNode? node) { - if (condition) return; - throw SourceSpanFormatException(message, node!.span); + static VersionConstraint _parseVersionConstraint(YamlNode node) { + return _parseNode(node, 'version constraint', + parse: VersionConstraint.parse); + } + + static String _getStringEntry(YamlMap map, String key) { + return _parseNode<String>( + _getEntry<YamlScalar>(map, key, 'string'), 'string'); + } + + static T _parseNode<T>(YamlNode node, String typeDescription, + {T Function(String)? parse}) { + if (node is T) { + return node as T; + } else if (node is YamlScalar) { + final value = node.value; + if (parse != null) { + if (value is! String) { + _failAt('Expected a $typeDescription.', node); + } + return _wrapFormatException( + 'Expected a $typeDescription.', node.span, () => parse(node.value)); + } else if (value is T) { + return value; + } + _failAt('Expected a $typeDescription.', node); + } + _failAt('Expected a $typeDescription.', node); + } + + static void _parseEachEntry<K, V>( + YamlMap map, + void Function(K key, V value) f, + String keyTypeDescription, + String valueTypeDescription) { + map.nodes.forEach((key, value) { + f(_parseNode(key, keyTypeDescription), + _parseNode(value, valueTypeDescription)); + }); + } + + static T _getEntry<T>( + YamlMap map, + String key, + String type, { + bool required = true, + }) { + final entry = map.nodes[key]; + // `null` here always means not present. A value explicitly mapped to `null` + // would be a `YamlScalar(null)`. + if (entry == null) { + if (required) { + _failAt('Expected a `$key` entry.', map); + } else { + return null as T; + } + } + return _parseNode(entry, type); + } + + static Never _failAt(String message, YamlNode node) { + throw SourceSpanFormatException(message, node.span); } /// Returns a copy of this LockFile with a package named [name] removed. @@ -241,12 +306,11 @@ rootUri = p.toUri(rootPath); } final pubspec = await cache.describe(id); - final sdkConstraint = pubspec.sdkConstraints[sdk.identifier]; entries.add(PackageConfigEntry( name: name, rootUri: rootUri, packageUri: p.toUri('lib/'), - languageVersion: LanguageVersion.fromSdkConstraint(sdkConstraint), + languageVersion: pubspec.languageVersion, )); } @@ -304,17 +368,22 @@ '''; } + static const _directMain = 'direct main'; + static const _directDev = 'direct dev'; + static const _directOverridden = 'direct overridden'; + static const _transitive = 'transitive'; + /// Returns the dependency classification for [package]. String _dependencyType(String package) { - if (_mainDependencies.contains(package)) return 'direct main'; - if (_devDependencies.contains(package)) return 'direct dev'; + if (_mainDependencies.contains(package)) return _directMain; + if (_devDependencies.contains(package)) return _directDev; // If a package appears in `dependency_overrides` and another dependency // section, the main section it appears in takes precedence. if (_overriddenDependencies.contains(package)) { - return 'direct overridden'; + return _directOverridden; } - return 'transitive'; + return _transitive; } /// `true` if [other] has the same packages as `this` in the same versions
diff --git a/lib/src/log.dart b/lib/src/log.dart index f845aa6..61c6768 100644 --- a/lib/src/log.dart +++ b/lib/src/log.dart
@@ -8,7 +8,6 @@ import 'dart:io'; import 'package:args/command_runner.dart'; -import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; @@ -341,7 +340,6 @@ /// replacing it with '[...]' if it is too long. /// /// [limit] must be more than 5. -@visibleForTesting String limitLength(String input, int limit) { const snip = '[...]'; assert(limit > snip.length);
diff --git a/lib/src/null_safety_analysis.dart b/lib/src/null_safety_analysis.dart index fe89b55..284e845 100644 --- a/lib/src/null_safety_analysis.dart +++ b/lib/src/null_safety_analysis.dart
@@ -91,7 +91,7 @@ } }, sources: _systemCache.sources, - sdkConstraints: {'dart': rootPubspec.sdkConstraints['dart']!})); + sdkConstraints: {'dart': rootPubspec.dartSdkConstraint})); final rootLanguageVersion = rootPubspec.languageVersion; if (!rootLanguageVersion.supportsNullSafety) {
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart index 0b6cd5b..69b5557 100644 --- a/lib/src/pubspec.dart +++ b/lib/src/pubspec.dart
@@ -120,6 +120,9 @@ Map<String, PackageRange>? _devDependencies; + /// The Dart sdk version this is parsed against. + final Version _dartSdkVersion; + /// The dependency constraints that this package overrides when it is the /// root package. /// @@ -133,7 +136,7 @@ if (pubspecOverridesFields != null) { pubspecOverridesFields.nodes.forEach((key, _) { if (!const {'dependency_overrides'}.contains(key.value)) { - throw PubspecException( + throw SourceSpanApplicationException( 'pubspec_overrides.yaml only supports the `dependency_overrides` field.', key.span, ); @@ -163,13 +166,13 @@ Map<String, PackageRange>? _dependencyOverrides; - /// A map from SDK identifiers to constraints on those SDK versions. - Map<String, VersionConstraint> get sdkConstraints { - _ensureEnvironment(); - return _sdkConstraints!; - } + SdkConstraint get dartSdkConstraint => sdkConstraints['dart']!; - Map<String, VersionConstraint>? _sdkConstraints; + /// A map from SDK identifiers to constraints on those SDK versions. + late final Map<String, SdkConstraint> sdkConstraints = + _givenSdkConstraints ?? UnmodifiableMapView(_parseEnvironment(fields)); + + final Map<String, SdkConstraint>? _givenSdkConstraints; /// Whether or not to apply the [_defaultUpperBoundsSdkConstraint] to this /// pubspec. @@ -177,41 +180,8 @@ /// 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 - /// `sdkConstraints["dart"]`. - VersionConstraint get originalDartSdkConstraint { - _ensureEnvironment(); - return _originalDartSdkConstraint ?? sdkConstraints['dart']!; - } - - VersionConstraint? _originalDartSdkConstraint; - - /// Ensures that the top-level "environment" field has been parsed and - /// [_sdkConstraints] is set accordingly. - void _ensureEnvironment() { - if (_sdkConstraints != null) return; - - var sdkConstraints = _parseEnvironment(fields); - var parsedDartSdkConstraint = sdkConstraints['dart']; - - if (parsedDartSdkConstraint is VersionRange && - _shouldEnableCurrentSdk(parsedDartSdkConstraint)) { - _originalDartSdkConstraint = parsedDartSdkConstraint; - _dartSdkWasOverridden = true; - sdkConstraints['dart'] = VersionRange( - min: parsedDartSdkConstraint.min, - includeMin: parsedDartSdkConstraint.includeMin, - max: sdk.version, - includeMax: true); - } - - _sdkConstraints = UnmodifiableMapView(sdkConstraints); - } + bool get dartSdkWasOverridden => _dartSdkWasOverriddenToAllowPrerelease; + bool _dartSdkWasOverriddenToAllowPrerelease = false; /// Whether or not we should override [sdkConstraint] to be <= the user's /// current SDK version. @@ -227,12 +197,12 @@ /// patch versions as the user's current SDK. bool _shouldEnableCurrentSdk(VersionRange sdkConstraint) { if (!_allowPreReleaseSdk) return false; - if (!sdk.version.isPreRelease) return false; + if (!_dartSdkVersion.isPreRelease) return false; if (sdkConstraint.includeMax) return false; var minSdkConstraint = sdkConstraint.min; if (minSdkConstraint != null && minSdkConstraint.isPreRelease && - equalsIgnoringPreRelease(sdkConstraint.min!, sdk.version)) { + equalsIgnoringPreRelease(sdkConstraint.min!, _dartSdkVersion)) { return false; } var maxSdkConstraint = sdkConstraint.max; @@ -241,59 +211,126 @@ !maxSdkConstraint.isFirstPreRelease) { return false; } - return equalsIgnoringPreRelease(maxSdkConstraint, sdk.version); + return equalsIgnoringPreRelease(maxSdkConstraint, _dartSdkVersion); + } + + SdkConstraint _interpretDartSdkConstraint( + VersionConstraint originalConstraint, { + required VersionConstraint? defaultUpperBoundConstraint, + }) { + VersionConstraint constraint = originalConstraint; + if (constraint is VersionRange && _shouldEnableCurrentSdk(constraint)) { + _dartSdkWasOverriddenToAllowPrerelease = true; + constraint = VersionRange( + min: constraint.min, + includeMin: constraint.includeMin, + max: _dartSdkVersion, + includeMax: true); + } + if (defaultUpperBoundConstraint != null && + constraint is VersionRange && + constraint.max == null && + defaultUpperBoundConstraint.allowsAny(constraint)) { + constraint = VersionConstraint.intersection( + [constraint, defaultUpperBoundConstraint]); + } + // If a package is null safe it should also be compatible with dart 3. + // Therefore we rewrite a null-safety enabled constraint with the upper + // bound <3.0.0 to be have upper bound <4.0.0 + if (constraint is VersionRange && + LanguageVersion.fromSdkConstraint(constraint) >= + LanguageVersion.firstVersionWithNullSafety && + // <3.0.0 is parsed into a max of 3.0.0-0, so that is what we look for + // here. + constraint.max == Version(3, 0, 0).firstPreRelease && + constraint.includeMax == false) { + constraint = VersionRange( + min: constraint.min, + includeMin: constraint.includeMin, + // We don't have to use .firstPreRelease as the constructor will do that + // if needed. + max: Version(4, 0, 0), + ); + } + return SdkConstraint(constraint, originalConstraint: originalConstraint); + } + + // Flutter constraints get special treatment, as Flutter won't be using + // semantic versioning to mark breaking releases. We simply ignore upper + // bounds. + SdkConstraint _interpretFlutterSdkConstraint(VersionConstraint constraint) { + if (constraint is VersionRange) { + return SdkConstraint( + VersionRange(min: constraint.min, includeMin: constraint.includeMin), + originalConstraint: constraint, + ); + } + return SdkConstraint(constraint); } /// Parses the "environment" field in [parent] and returns a map from SDK /// identifiers to constraints on those SDKs. - Map<String, VersionConstraint> _parseEnvironment(YamlMap parent) { + Map<String, SdkConstraint> _parseEnvironment(YamlMap parent) { var yaml = parent['environment']; + final VersionConstraint originalDartSdkConstraint; if (yaml == null) { - return { - 'dart': _includeDefaultSdkConstraint - ? _defaultUpperBoundSdkConstraint - : VersionConstraint.any - }; - } - - if (yaml is! YamlMap) { + originalDartSdkConstraint = _includeDefaultSdkConstraint + ? _defaultUpperBoundSdkConstraint + : VersionConstraint.any; + } else if (yaml is! YamlMap) { _error('"environment" field must be a map.', parent.nodes['environment']!.span); + } else { + originalDartSdkConstraint = _parseVersionConstraint( + yaml.nodes['sdk'], + _packageName, + _FileType.pubspec, + ); } var constraints = { - 'dart': _parseVersionConstraint( - yaml.nodes['sdk'], _packageName, _FileType.pubspec, - defaultUpperBoundConstraint: _includeDefaultSdkConstraint - ? _defaultUpperBoundSdkConstraint - : null) + 'dart': _interpretDartSdkConstraint( + originalDartSdkConstraint, + defaultUpperBoundConstraint: _includeDefaultSdkConstraint + ? _defaultUpperBoundSdkConstraint + : null, + ) }; - yaml.nodes.forEach((name, constraint) { - if (name.value is! String) { - _error('SDK names must be strings.', name.span); - } else if (name.value == 'dart') { - _error('Use "sdk" to for Dart SDK constraints.', name.span); - } - if (name.value == 'sdk') return; - constraints[name.value as String] = - _parseVersionConstraint(constraint, _packageName, _FileType.pubspec, - // Flutter constraints get special treatment, as Flutter won't be - // using semantic versioning to mark breaking releases. - ignoreUpperBound: name.value == 'flutter'); - }); + if (yaml is YamlMap) { + yaml.nodes.forEach((nameNode, constraintNode) { + final name = nameNode.value; + if (name is! String) { + _error('SDK names must be strings.', nameNode.span); + } else if (name == 'dart') { + _error('Use "sdk" to for Dart SDK constraints.', nameNode.span); + } + if (name == 'sdk') return; + final constraint = _parseVersionConstraint( + constraintNode, + _packageName, + _FileType.pubspec, + ); + constraints[name] = name == 'flutter' + ? _interpretFlutterSdkConstraint(constraint) + : SdkConstraint(constraint); + }); + } return constraints; } /// The language version implied by the sdk constraint. - LanguageVersion get languageVersion => - LanguageVersion.fromSdkConstraint(originalDartSdkConstraint); + LanguageVersion get languageVersion { + return LanguageVersion.fromSdkConstraint( + dartSdkConstraint.originalConstraint, + ); + } /// Loads the pubspec for a package located in [packageDir]. /// /// If [expectedName] is passed and the pubspec doesn't have a matching name - /// field, this will throw a [PubspecException]. + /// field, this will throw a [SourceSpanApplicationException]. /// /// If [allowOverridesFile] is `true` [pubspecOverridesFilename] is loaded and /// is allowed to override dependency_overrides from `pubspec.yaml`. @@ -332,7 +369,8 @@ Iterable<PackageRange>? dependencyOverrides, Map? fields, SourceRegistry? sources, - Map<String, VersionConstraint>? sdkConstraints, + Map<String, SdkConstraint>? sdkConstraints, + Version? dartSdkVersion, }) : _dependencies = dependencies == null ? null : Map.fromIterable(dependencies, key: (range) => range.name), @@ -342,12 +380,13 @@ _dependencyOverrides = dependencyOverrides == null ? null : Map.fromIterable(dependencyOverrides, key: (range) => range.name), - _sdkConstraints = sdkConstraints ?? - UnmodifiableMapView({'dart': VersionConstraint.any}), + _givenSdkConstraints = sdkConstraints ?? + UnmodifiableMapView({'dart': SdkConstraint(VersionConstraint.any)}), _includeDefaultSdkConstraint = false, _sources = sources ?? ((String? name) => throw StateError('No source registry given')), _overridesFileFields = null, + _dartSdkVersion = dartSdkVersion ?? sdk.version, super( fields == null ? YamlMap() : YamlMap.wrap(fields), name: name, @@ -361,10 +400,17 @@ /// field, this will throw a [PubspecError]. /// /// [location] is the location from which this pubspec was loaded. - Pubspec.fromMap(Map fields, this._sources, - {YamlMap? overridesFields, String? expectedName, Uri? location}) - : _overridesFileFields = overridesFields, + Pubspec.fromMap( + Map fields, + this._sources, { + YamlMap? overridesFields, + String? expectedName, + Uri? location, + Version? dartSdkVersion, + }) : _overridesFileFields = overridesFields, _includeDefaultSdkConstraint = true, + _givenSdkConstraints = null, + _dartSdkVersion = dartSdkVersion ?? sdk.version, super(fields is YamlMap ? fields : YamlMap.wrap(fields, sourceUrl: location)) { @@ -373,7 +419,7 @@ if (expectedName == null) return; if (name == expectedName) return; - throw PubspecException( + throw SourceSpanApplicationException( '"name" field doesn\'t match expected name ' '"$expectedName".', this.fields.nodes['name']!.span); @@ -390,6 +436,7 @@ Uri? location, String? overridesFileContents, Uri? overridesLocation, + Version? dartSdkVersion, }) { late final YamlMap pubspecMap; YamlMap? overridesFileMap; @@ -400,13 +447,17 @@ loadYamlNode(overridesFileContents, sourceUrl: overridesLocation)); } } on YamlException catch (error) { - throw PubspecException(error.message, error.span); + throw SourceSpanApplicationException(error.message, error.span); } - return Pubspec.fromMap(pubspecMap, sources, - overridesFields: overridesFileMap, - expectedName: expectedName, - location: location); + return Pubspec.fromMap( + pubspecMap, + sources, + overridesFields: overridesFileMap, + expectedName: expectedName, + location: location, + dartSdkVersion: dartSdkVersion, + ); } /// Ensures that [node] is a mapping. @@ -420,19 +471,20 @@ } else if (node is YamlMap) { return node; } else { - throw PubspecException('The pubspec must be a YAML mapping.', node.span); + throw SourceSpanApplicationException( + 'The pubspec must be a YAML mapping.', node.span); } } /// Returns a list of most errors in this pubspec. /// /// This will return at most one error for each field. - List<PubspecException> get allErrors { - var errors = <PubspecException>[]; + List<SourceSpanApplicationException> get allErrors { + var errors = <SourceSpanApplicationException>[]; void collectError(void Function() fn) { try { fn(); - } on PubspecException catch (e) { + } on SourceSpanApplicationException catch (e) { errors.add(e); } } @@ -444,7 +496,7 @@ collectError(() => publishTo); collectError(() => executables); collectError(() => falseSecrets); - collectError(_ensureEnvironment); + collectError(() => sdkConstraints); return errors; } } @@ -557,17 +609,13 @@ /// Parses [node] to a [VersionConstraint]. /// -/// If or [defaultUpperBoundConstraint] is specified then it will be set as -/// the max constraint if the original constraint doesn't have an upper -/// bound and it is compatible with [defaultUpperBoundConstraint]. -/// -/// If [ignoreUpperBound] the max constraint is ignored. +/// If or [defaultUpperBoundConstraint] is specified then it will be set as the +/// max constraint if the original constraint doesn't have an upper bound and it +/// is compatible with [defaultUpperBoundConstraint]. VersionConstraint _parseVersionConstraint( - YamlNode? node, String? packageName, _FileType fileType, - {VersionConstraint? defaultUpperBoundConstraint, - bool ignoreUpperBound = false}) { + YamlNode? node, String? packageName, _FileType fileType) { if (node?.value == null) { - return defaultUpperBoundConstraint ?? VersionConstraint.any; + return VersionConstraint.any; } if (node!.value is! String) { _error('A version constraint must be a string.', node.span); @@ -575,23 +623,12 @@ return _wrapFormatException('version constraint', node.span, () { var constraint = VersionConstraint.parse(node.value); - if (defaultUpperBoundConstraint != null && - constraint is VersionRange && - constraint.max == null && - defaultUpperBoundConstraint.allowsAny(constraint)) { - constraint = VersionConstraint.intersection( - [constraint, defaultUpperBoundConstraint]); - } - if (ignoreUpperBound && constraint is VersionRange) { - return VersionRange( - min: constraint.min, includeMin: constraint.includeMin); - } return constraint; }, packageName, fileType); } /// Runs [fn] and wraps any [FormatException] it throws in a -/// [PubspecException]. +/// [SourceSpanApplicationException]. /// /// [description] should be a noun phrase that describes whatever's being /// parsed or processed by [fn]. [span] should be the location of whatever's @@ -611,7 +648,7 @@ return fn(); } on FormatException catch (e) { // If we already have a pub exception with a span, re-use that - if (e is PubspecException) rethrow; + if (e is SourceSpanApplicationException) rethrow; var msg = 'Invalid $description'; final typeName = _fileTypeName(fileType); @@ -624,9 +661,9 @@ } } -/// Throws a [PubspecException] with the given message. +/// Throws a [SourceSpanApplicationException] with the given message. Never _error(String message, SourceSpan? span) { - throw PubspecException(message, span); + throw SourceSpanApplicationException(message, span); } enum _FileType { @@ -642,3 +679,40 @@ return 'pubspec override'; } } + +/// There are special rules or interpreting SDK constraints, we take care to +/// save the original constraint as found in pubspec.yaml. +class SdkConstraint { + /// The constraint as written in the pubspec.yaml. + final VersionConstraint originalConstraint; + + /// The constraint as interpreted by pub. + final VersionConstraint effectiveConstraint; + + SdkConstraint(this.effectiveConstraint, + {VersionConstraint? originalConstraint}) + : originalConstraint = originalConstraint ?? effectiveConstraint; + + /// The language version of a constraint is determined from how it is written. + LanguageVersion get languageVersion => + LanguageVersion.fromSdkConstraint(originalConstraint); + + // We currently don't call this anywhere - so this is only for debugging + // purposes. + @override + String toString() { + if (effectiveConstraint != originalConstraint) { + return '$originalConstraint (interpreted as $effectiveConstraint)'; + } + return effectiveConstraint.toString(); + } + + @override + operator ==(other) => + other is SdkConstraint && + other.effectiveConstraint == effectiveConstraint && + other.originalConstraint == originalConstraint; + + @override + int get hashCode => Object.hash(effectiveConstraint, originalConstraint); +}
diff --git a/lib/src/pubspec_parse.dart b/lib/src/pubspec_parse.dart index c86a706..c6babfb 100644 --- a/lib/src/pubspec_parse.dart +++ b/lib/src/pubspec_parse.dart
@@ -7,7 +7,7 @@ import 'package:source_span/source_span.dart'; import 'package:yaml/yaml.dart'; -import 'exceptions.dart' show ApplicationException; +import 'exceptions.dart'; import 'utils.dart' show identifierRegExp, reservedWords; /// A regular expression matching allowed package names. @@ -42,15 +42,18 @@ String _lookupName() { final name = fields['name']; if (name == null) { - throw PubspecException('Missing the required "name" field.', fields.span); + throw SourceSpanApplicationException( + 'Missing the required "name" field.', fields.span); } else if (name is! String) { - throw PubspecException( + throw SourceSpanApplicationException( '"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.', + throw SourceSpanApplicationException( + '"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.', + throw SourceSpanApplicationException( + '"name" field may not be a Dart reserved word.', fields.nodes['name']?.span); } @@ -223,7 +226,7 @@ bool get isPrivate => publishTo == 'none'; /// Runs [fn] and wraps any [FormatException] it throws in a - /// [PubspecException]. + /// [SourceSpanApplicationException]. /// /// [description] should be a noun phrase that describes whatever's being /// parsed or processed by [fn]. [span] should be the location of whatever's @@ -242,21 +245,13 @@ msg = '$msg in the "$name" pubspec on the "$targetPackage" dependency'; } msg = '$msg: ${e.message}'; - throw PubspecException(msg, span); + throw SourceSpanApplicationException(msg, span); } } - /// Throws a [PubspecException] with the given message. + /// Throws a [SourceSpanApplicationException] with the given message. @alwaysThrows void _error(String message, SourceSpan? span) { - throw PubspecException(message, span); + throw SourceSpanApplicationException(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/solver/package_lister.dart b/lib/src/solver/package_lister.dart index ce2b4c1..f3b63b6 100644 --- a/lib/src/solver/package_lister.dart +++ b/lib/src/solver/package_lister.dart
@@ -190,7 +190,7 @@ try { pubspec = await withDependencyType( _dependencyType, () => _systemCache.describe(id)); - } on PubspecException catch (error) { + } on SourceSpanApplicationException catch (error) { // The lockfile for the pubspec couldn't be parsed, log.fine('Failed to parse pubspec for $id:\n$error'); _knownInvalidVersions = _knownInvalidVersions.union(id.version); @@ -218,8 +218,11 @@ for (var sdk in sdks.values) { if (!_matchesSdkConstraint(pubspec, sdk)) { return [ - Incompatibility([Term(depender, true)], - SdkCause(pubspec.sdkConstraints[sdk.identifier], sdk)) + Incompatibility( + [Term(depender, true)], + SdkCause( + pubspec.sdkConstraints[sdk.identifier]?.effectiveConstraint, + sdk)) ]; } } @@ -321,12 +324,13 @@ alwaysIncludeMaxPreRelease: true); _knownInvalidVersions = incompatibleVersions.union(_knownInvalidVersions); - var sdkConstraint = await foldAsync( + var sdkConstraint = await foldAsync<VersionConstraint, PackageId>( slice(versions, bounds.first, bounds.last + 1), VersionConstraint.empty, - (dynamic previous, dynamic version) async { + (previous, version) async { var pubspec = await _describeSafe(version); return previous.union( - pubspec.sdkConstraints[sdk.identifier] ?? VersionConstraint.any); + pubspec.sdkConstraints[sdk.identifier]?.effectiveConstraint ?? + VersionConstraint.any); }); return Incompatibility( @@ -428,6 +432,7 @@ var constraint = pubspec.sdkConstraints[sdk.identifier]; if (constraint == null) return true; - return sdk.isAvailable && constraint.allows(sdk.version!); + return sdk.isAvailable && + constraint.effectiveConstraint.allows(sdk.version!); } }
diff --git a/lib/src/solver/result.dart b/lib/src/solver/result.dart index 7d57c36..df3aa4a 100644 --- a/lib/src/solver/result.dart +++ b/lib/src/solver/result.dart
@@ -63,7 +63,7 @@ var sdkConstraints = <String, VersionConstraint>{}; for (var pubspec in nonOverrides) { pubspec.sdkConstraints.forEach((identifier, constraint) { - sdkConstraints[identifier] = constraint + sdkConstraints[identifier] = constraint.effectiveConstraint .intersect(sdkConstraints[identifier] ?? VersionConstraint.any); }); }
diff --git a/lib/src/validator.dart b/lib/src/validator.dart index ef04bc4..cbf930f 100644 --- a/lib/src/validator.dart +++ b/lib/src/validator.dart
@@ -11,6 +11,7 @@ import 'entrypoint.dart'; import 'log.dart' as log; import 'sdk.dart'; +import 'validator/analyze.dart'; import 'validator/changelog.dart'; import 'validator/compiled_dartdoc.dart'; import 'validator/dependency.dart'; @@ -74,7 +75,7 @@ void validateSdkConstraint(Version firstSdkVersion, String message) { // If the SDK constraint disallowed all versions before [firstSdkVersion], // no error is necessary. - if (entrypoint.root.pubspec.originalDartSdkConstraint + if (entrypoint.root.pubspec.dartSdkConstraint.originalConstraint .intersect(VersionRange(max: firstSdkVersion)) .isEmpty) { return; @@ -95,7 +96,8 @@ ? firstSdkVersion.nextPatch : firstSdkVersion.nextBreaking); - var newSdkConstraint = entrypoint.root.pubspec.originalDartSdkConstraint + var newSdkConstraint = entrypoint + .root.pubspec.dartSdkConstraint.originalConstraint .intersect(allowedSdks); if (newSdkConstraint.isEmpty) newSdkConstraint = allowedSdks; @@ -130,6 +132,7 @@ required List<String> warnings, required List<String> errors}) async { var validators = [ + AnalyzeValidator(), GitignoreValidator(), PubspecValidator(), LicenseValidator(),
diff --git a/lib/src/validator/analyze.dart b/lib/src/validator/analyze.dart new file mode 100644 index 0000000..b9af306 --- /dev/null +++ b/lib/src/validator/analyze.dart
@@ -0,0 +1,30 @@ +// Copyright (c) 2022, 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 '../io.dart'; + +import '../log.dart'; +import '../validator.dart'; + +/// Runs `dart analyze` and gives a warning if it returns non-zero. +class AnalyzeValidator extends Validator { + @override + Future<void> validate() async { + final result = await runProcess(Platform.resolvedExecutable, [ + 'analyze', + '--fatal-infos', + if (!p.equals(entrypoint.root.dir, p.current)) entrypoint.root.dir, + ]); + if (result.exitCode != 0) { + final limitedOutput = limitLength(result.stdout.join('\n'), 1000); + warnings + .add('`dart analyze` found the following issue(s):\n$limitedOutput'); + } + } +}
diff --git a/lib/src/validator/flutter_plugin_format.dart b/lib/src/validator/flutter_plugin_format.dart index a56f7df..00160ca 100644 --- a/lib/src/validator/flutter_plugin_format.dart +++ b/lib/src/validator/flutter_plugin_format.dart
@@ -46,7 +46,7 @@ final flutterConstraint = pubspec.sdkConstraints['flutter']; if (usesNewPluginFormat && (flutterConstraint == null || - flutterConstraint.allowsAny(VersionRange( + flutterConstraint.effectiveConstraint.allowsAny(VersionRange( min: Version.parse('0.0.0'), max: Version.parse('1.10.0'), includeMin: true,
diff --git a/pubspec.yaml b/pubspec.yaml index 4f52414..eb2983f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml
@@ -6,7 +6,7 @@ dependencies: # Note: Pub's test infrastructure assumes that any dependencies used in tests # will be hosted dependencies. - analyzer: ^4.0.0 + analyzer: ^5.1.0 args: ^2.1.0 async: ^2.6.1 cli_util: ^0.3.5
diff --git a/test/dart3_sdk_constraint_hack_test.dart b/test/dart3_sdk_constraint_hack_test.dart new file mode 100644 index 0000000..7df3ee8 --- /dev/null +++ b/test/dart3_sdk_constraint_hack_test.dart
@@ -0,0 +1,111 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test/test.dart'; + +import 'descriptor.dart' as d; +import 'test_pub.dart'; + +void main() { + test('The bound of ">=2.11.0 <3.0.0" is not modified', () async { + await d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'environment': {'sdk': '>=2.11.0 <3.0.0'} + }), + ]).create(); + + await pubGet( + error: contains( + 'Because myapp requires SDK version >=2.11.0 <3.0.0, version solving failed'), + environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, + ); + }); + test('The bound of ">=2.12.0 <3.1.0" is not modified', () async { + await d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'environment': {'sdk': '>=2.12.0 <3.1.0'} + }), + ]).create(); + + await pubGet( + error: contains( + 'Because myapp requires SDK version >=2.12.0 <3.1.0, version solving failed'), + environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, + ); + }); + + test('The bound of ">=2.11.0 <2.999.0" is not modified', () async { + await d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'environment': {'sdk': '>=2.11.0 <2.999.0'} + }), + ]).create(); + + await pubGet( + error: contains( + 'Because myapp requires SDK version >=2.11.0 <2.999.0, version solving failed'), + environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, + ); + }); + + test('The bound of ">=2.11.0 <3.0.0-0.0" is not modified', () async { + await d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'environment': {'sdk': '>=2.11.0 <3.0.0-0.0'} + }), + ]).create(); + + await pubGet( + error: contains( + 'Because myapp requires SDK version >=2.11.0 <3.0.0-0.0, version solving failed'), + environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, + ); + }); + + test('The bound of ">=2.12.0 <3.0.0" is modified', () async { + await d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'environment': {'sdk': '>=2.12.0 <3.0.0'} + }), + ]).create(); + + await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}); + }); + + test('The bound of ">=2.12.0 <3.0.0-0" is modified', () async { + // For the upper bound <3.0.0 is treated as <3.0.0-0, so they both have + // the rewrite applied. + await d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'environment': {'sdk': '>=2.12.0 <3.0.0-0'} + }), + ]).create(); + + await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}); + }); + + test( + 'The bound of ">=2.12.0 <3.0.0" is not compatible with prereleases of dart 4', + () async { + await d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'environment': {'sdk': '>=2.12.0 <3.0.0'} + }), + ]).create(); + + await pubGet( + environment: {'_PUB_TEST_SDK_VERSION': '4.0.0-alpha'}, + error: contains( + 'Because myapp requires SDK version >=2.12.0 <4.0.0, version solving failed.', + ), + ); + }); +}
diff --git a/test/list_package_dirs/lockfile_error_test.dart b/test/list_package_dirs/lockfile_error_test.dart index f4440ec..5c7bff9 100644 --- a/test/list_package_dirs/lockfile_error_test.dart +++ b/test/list_package_dirs/lockfile_error_test.dart
@@ -19,7 +19,7 @@ 'list-package-dirs', '--format=json' ], outputJson: { - 'error': contains('The lockfile must be a YAML mapping.'), + 'error': contains('Expected a YAML mapping.'), 'path': canonicalize(path.join(d.sandbox, appPath, 'pubspec.lock')) }, exitCode: exit_codes.DATA); });
diff --git a/test/pubspec_test.dart b/test/pubspec_test.dart index 2ccb30f..23079c5 100644 --- a/test/pubspec_test.dart +++ b/test/pubspec_test.dart
@@ -4,8 +4,8 @@ import 'dart:io'; +import 'package:pub/src/exceptions.dart'; import 'package:pub/src/pubspec.dart'; -import 'package:pub/src/sdk.dart'; import 'package:pub/src/source/hosted.dart'; import 'package:pub/src/system_cache.dart'; import 'package:pub_semver/pub_semver.dart'; @@ -15,11 +15,12 @@ group('parse()', () { final sources = SystemCache().sources; - var throwsPubspecException = throwsA(const TypeMatcher<PubspecException>()); + var throwsPubspecException = + throwsA(const TypeMatcher<SourceSpanApplicationException>()); void expectPubspecException(String contents, void Function(Pubspec) fn, [String? expectedContains]) { - var expectation = const TypeMatcher<PubspecException>(); + var expectation = const TypeMatcher<SourceSpanApplicationException>(); if (expectedContains != null) { expectation = expectation.having( (error) => error.message, 'message', contains(expectedContains)); @@ -390,7 +391,7 @@ expect( () => pubspec.dependencies, throwsA( - isA<PubspecException>() + isA<SourceSpanApplicationException>() .having((e) => e.span!.text, 'span.text', 'invalid value'), ), ); @@ -493,36 +494,23 @@ }); group('environment', () { - /// Checking for the default SDK constraint based on the current SDK. - void expectDefaultSdkConstraint(Pubspec pubspec) { - var sdkVersionString = sdk.version.toString(); - if (sdkVersionString.startsWith('2.0.0') && sdk.version.isPreRelease) { - expect( - pubspec.sdkConstraints, - containsPair( - 'dart', - VersionConstraint.parse( - '${pubspec.sdkConstraints["dart"]} <=$sdkVersionString'))); - } else { - expect( - pubspec.sdkConstraints, - containsPair( - 'dart', - VersionConstraint.parse( - "${pubspec.sdkConstraints["dart"]} <2.0.0"))); - } - } - test('allows an omitted environment', () { var pubspec = Pubspec.parse('name: testing', sources); - expectDefaultSdkConstraint(pubspec); + expect( + pubspec.dartSdkConstraint.effectiveConstraint, + VersionConstraint.parse('<2.0.0'), + ); + expect(pubspec.sdkConstraints, isNot(contains('flutter'))); expect(pubspec.sdkConstraints, isNot(contains('fuchsia'))); }); test('default SDK constraint can be omitted with empty environment', () { var pubspec = Pubspec.parse('', sources); - expectDefaultSdkConstraint(pubspec); + expect( + pubspec.dartSdkConstraint.effectiveConstraint, + VersionConstraint.parse('<2.0.0'), + ); expect(pubspec.sdkConstraints, isNot(contains('flutter'))); expect(pubspec.sdkConstraints, isNot(contains('fuchsia'))); }); @@ -533,7 +521,10 @@ environment: sdk: ">1.0.0" ''', sources); - expectDefaultSdkConstraint(pubspec); + expect( + pubspec.dartSdkConstraint.effectiveConstraint, + VersionConstraint.parse('>1.0.0 <2.0.0'), + ); expect(pubspec.sdkConstraints, isNot(contains('flutter'))); expect(pubspec.sdkConstraints, isNot(contains('fuchsia'))); }); @@ -545,8 +536,12 @@ environment: sdk: ">3.0.0" ''', sources); - expect(pubspec.sdkConstraints, - containsPair('dart', VersionConstraint.parse('>3.0.0'))); + expect( + pubspec.sdkConstraints, + containsPair( + 'dart', + SdkConstraint(VersionConstraint.parse('>3.0.0')), + )); expect(pubspec.sdkConstraints, isNot(contains('flutter'))); expect(pubspec.sdkConstraints, isNot(contains('fuchsia'))); }); @@ -563,12 +558,23 @@ flutter: ^0.1.2 fuchsia: ^5.6.7 ''', sources); - expect(pubspec.sdkConstraints, - containsPair('dart', VersionConstraint.parse('>=1.2.3 <2.3.4'))); - expect(pubspec.sdkConstraints, - containsPair('flutter', VersionConstraint.parse('>=0.1.2'))); - expect(pubspec.sdkConstraints, - containsPair('fuchsia', VersionConstraint.parse('^5.6.7'))); + expect( + pubspec.sdkConstraints, + containsPair('dart', + SdkConstraint(VersionConstraint.parse('>=1.2.3 <2.3.4')))); + expect( + pubspec.sdkConstraints, + containsPair( + 'flutter', + SdkConstraint( + VersionConstraint.parse('>=0.1.2'), + originalConstraint: VersionConstraint.parse('^0.1.2'), + ))); + expect( + pubspec.sdkConstraints, + containsPair( + 'fuchsia', SdkConstraint(VersionConstraint.parse('^5.6.7'))), + ); }); test("throws if the sdk isn't a string", () { @@ -700,7 +706,7 @@ void Function(Pubspec) fn, [ String? expectedContains, ]) { - var expectation = isA<PubspecException>(); + var expectation = isA<SourceSpanApplicationException>(); if (expectedContains != null) { expectation = expectation.having((error) => error.toString(), 'toString()', contains(expectedContains));
diff --git a/test/upgrade/upgrade_null_safety_test.dart b/test/upgrade/upgrade_null_safety_test.dart index 557c589..ad210d8 100644 --- a/test/upgrade/upgrade_null_safety_test.dart +++ b/test/upgrade/upgrade_null_safety_test.dart
@@ -441,7 +441,7 @@ }, error: allOf( contains('Because myapp depends on has_conflict >=2.0.0 which'), - contains('requires SDK version >=2.13.0 <3.0.0,'), + contains('requires SDK version >=2.13.0 <4.0.0,'), contains('version solving failed.'), ), );
diff --git a/test/validator/analyze_test.dart b/test/validator/analyze_test.dart new file mode 100644 index 0000000..0717b3c --- /dev/null +++ b/test/validator/analyze_test.dart
@@ -0,0 +1,102 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:pub/src/exit_codes.dart'; +import 'package:test/test.dart'; + +import '../descriptor.dart' as d; +import '../test_pub.dart'; + +Future<void> expectValidation( + error, + int exitCode, { + List<String> extraArgs = const [], + Map<String, String> environment = const {}, + String? workingDirectory, +}) async { + await runPub( + error: error, + args: ['publish', '--dry-run', ...extraArgs], + environment: {'_PUB_TEST_SDK_VERSION': '1.12.0', ...environment}, + workingDirectory: workingDirectory ?? d.path(appPath), + exitCode: exitCode, + ); +} + +void main() { + test('should consider a package valid if it contains no warnings or errors', + () async { + await d.dir(appPath, [ + d.libPubspec('test_pkg', '1.0.0', sdk: '>=1.8.0 <=2.0.0'), + d.file('LICENSE', 'Eh, do what you want.'), + d.file('README.md', "This package isn't real."), + d.file('CHANGELOG.md', '# 1.0.0\nFirst version\n'), + d.dir('lib', [d.file('test_pkg.dart', 'int i = 1;')]) + ]).create(); + + await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '1.12.0'}); + + await expectValidation(contains('Package has 0 warnings.'), 0); + }); + + test('should warn if package contains errors, and works with --directory', + () async { + await d.dir(appPath, [ + d.libPubspec('test_pkg', '1.0.0', sdk: '>=1.8.0 <=2.0.0'), + d.file('LICENSE', 'Eh, do what you want.'), + d.file('README.md', "This package isn't real."), + d.file('CHANGELOG.md', '# 1.0.0\nFirst version\n'), + d.dir('lib', [ + d.file('test_pkg.dart', ''' +void main() { +// Missing } +''') + ]) + ]).create(); + + await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '1.12.0'}); + + await expectValidation( + allOf([ + contains('`dart analyze` found the following issue(s):'), + contains('Analyzing myapp...'), + contains('error -'), + contains("Expected to find '}'."), + contains('Package has 1 warning.') + ]), + DATA, + extraArgs: ['--directory', appPath], + workingDirectory: d.sandbox, + ); + }); + + test('should warn if package contains infos', () async { + await d.dir(appPath, [ + d.libPubspec('test_pkg', '1.0.0', sdk: '>=1.8.0 <=2.0.0'), + d.file('LICENSE', 'Eh, do what you want.'), + d.file('README.md', "This package isn't real."), + d.file('CHANGELOG.md', '# 1.0.0\nFirst version\n'), + d.dir('lib', [ + d.file('test_pkg.dart', ''' +void main() { + final a = 10; // Unused. +} +''') + ]), + ]).create(); + + await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '1.12.0'}); + + await expectValidation( + allOf([ + contains('`dart analyze` found the following issue(s):'), + contains('Analyzing myapp...'), + contains('info -'), + contains("The value of the local variable 'a' isn't used"), + contains('Package has 1 warning.') + ]), + DATA, + ); + }); +}
diff --git a/test/validator/dependency_test.dart b/test/validator/dependency_test.dart index 69fc4a8..b3adc34 100644 --- a/test/validator/dependency_test.dart +++ b/test/validator/dependency_test.dart
@@ -39,10 +39,10 @@ } Future<void> expectValidationWarning(error, - {Map<String, String> environment = const {}}) async { + {int count = 1, Map<String, String> environment = const {}}) async { if (error is String) error = contains(error); await expectValidation( - error: allOf([error, contains('Package has 1 warning.')]), + error: allOf([error, contains('Package has $count warning')]), exitCode: DATA, environment: environment, ); @@ -106,7 +106,9 @@ await expectValidationWarning( allOf([ contains(' foo: any'), + contains("Publishable packages can't have 'git' dependencies"), ]), + count: 2, environment: {'_PUB_TEST_SDK_VERSION': '2.0.0'}); });
diff --git a/test/version_solver_test.dart b/test/version_solver_test.dart index 69489a3..8ade3f6 100644 --- a/test/version_solver_test.dart +++ b/test/version_solver_test.dart
@@ -1172,7 +1172,7 @@ await expectResolves( environment: {'_PUB_TEST_SDK_VERSION': '2.0.0-dev.99'}, - // Log output should mention the PUB_ALLOW_RELEASE_SDK environment + // Log output should mention the PUB_ALLOW_PRERELEASE_SDK environment // variable and mention the foo and bar packages specifically. output: allOf(contains('PUB_ALLOW_PRERELEASE_SDK'), anyOf(contains('bar, foo'))),