blob: ac74da6516303190595ceb8feb30bd72605c4e89 [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:path/path.dart' as p;
import '../command.dart';
import '../command_runner.dart';
import '../entrypoint.dart';
import '../io.dart';
import '../lock_file.dart';
import '../log.dart' as log;
import '../log.dart';
import '../package.dart';
import '../package_name.dart';
import '../pubspec.dart';
import '../pubspec_utils.dart';
import '../solver.dart';
import '../source/git.dart';
import '../source/hosted.dart';
import '../source/path.dart';
import '../source/sdk.dart' show SdkSource;
import '../system_cache.dart';
import '../utils.dart';
class OutdatedCommand extends PubCommand {
@override
String get name => 'outdated';
@override
String get description =>
'Analyze your dependencies to find which ones can be upgraded.';
@override
String get argumentsDescription => '[options]';
@override
String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-outdated';
/// Avoid showing spinning progress messages when not in a terminal, and
/// when we are outputting machine-readable json.
bool get _shouldShowSpinner =>
terminalOutputForStdout && !argResults.flag('json');
@override
bool get takesArguments => false;
OutdatedCommand() {
argParser.addFlag(
'dependency-overrides',
defaultsTo: true,
help: 'Show resolutions with `dependency_overrides`.',
);
argParser.addFlag(
'dev-dependencies',
defaultsTo: true,
help: 'Take dev dependencies into account.',
);
argParser.addFlag(
'json',
help: 'Output the results using a json format.',
negatable: false,
);
argParser.addOption(
'mode',
help: '''
Highlight versions with PROPERTY.
Only packages currently missing that PROPERTY will be included unless
--show-all.
''',
valueHelp: 'PROPERTY',
allowed: ['outdated', 'null-safety'],
defaultsTo: 'outdated',
hide: true,
);
argParser.addFlag(
'prereleases',
help: 'Include prereleases in latest version.',
);
// Preserve for backwards compatibility.
argParser.addFlag(
'pre-releases',
help: 'Alias of prereleases.',
hide: true,
);
argParser.addFlag(
'show-all',
help: 'Include dependencies that are already fulfilling --mode.',
);
// Preserve for backwards compatibility.
argParser.addFlag(
'up-to-date',
hide: true,
help:
'Include dependencies that are already at the '
'latest version. Alias of --show-all.',
);
argParser.addFlag(
'transitive',
help: 'Show transitive dependencies.',
defaultsTo: true,
);
argParser.addOption(
'directory',
abbr: 'C',
help: 'Run this in the directory <dir>.',
valueHelp: 'dir',
);
}
@override
Future<void> runProtected() async {
if (argResults.option('mode') == 'null-safety') {
dataError('''The `--mode=null-safety` option is no longer supported.
Consider using the Dart 2.19 sdk to migrate to null safety.''');
}
final mode = _OutdatedMode();
final includeDevDependencies = argResults.flag('dev-dependencies');
final includeDependencyOverrides = argResults.flag('dependency-overrides');
if (argResults.flag('json') && argResults.wasParsed('transitive')) {
usageException(
'Cannot specify both `--json` and `--transitive`\n'
'The json report always includes transitive dependencies.',
);
}
/// The workspace root with dependency overrides removed if requested.
final baseWorkspace =
includeDependencyOverrides
? entrypoint.workspaceRoot
: entrypoint.workspaceRoot.transformWorkspace(
(package) => stripDependencyOverrides(package.pubspec),
);
/// [baseWorkspace] with dev-dependencies removed if requested.
final upgradableWorkspace =
includeDevDependencies
? baseWorkspace
: baseWorkspace.transformWorkspace(
(package) => stripDevDependencies(package.pubspec),
);
/// [upgradableWorkspace] with upper bounds removed.
final resolvableWorkspace = upgradableWorkspace.transformWorkspace(
(package) => mode.resolvablePubspec(package.pubspec),
);
late List<PackageId> upgradablePackages;
late List<PackageId> resolvablePackages;
late bool hasUpgradableResolution;
late bool hasResolvableResolution;
await log.spinner('Resolving', () async {
final upgradablePackagesResult = await _tryResolve(
upgradableWorkspace,
cache,
lockFile: entrypoint.lockFile,
);
hasUpgradableResolution = upgradablePackagesResult != null;
upgradablePackages = upgradablePackagesResult ?? [];
final resolvablePackagesResult = await _tryResolve(
resolvableWorkspace,
cache,
lockFile: entrypoint.lockFile,
);
hasResolvableResolution = resolvablePackagesResult != null;
resolvablePackages = resolvablePackagesResult ?? [];
}, condition: _shouldShowSpinner);
// This list will be empty if there is no lock file.
final currentPackages = entrypoint.lockFile.packages.values;
/// The set of all dependencies (direct and transitive) that are in the
/// closure of the non-dev dependencies from the root in at least one of
/// the current, upgradable and resolvable resolutions.
final nonDevDependencies = <String>{
...await _nonDevDependencyClosure(
entrypoint.workspaceRoot,
currentPackages,
),
...await _nonDevDependencyClosure(
entrypoint.workspaceRoot,
upgradablePackages,
),
...await _nonDevDependencyClosure(
entrypoint.workspaceRoot,
resolvablePackages,
),
};
Future<_PackageDetails> analyzeDependency(PackageRef packageRef) async {
final name = packageRef.name;
final current = entrypoint.lockFile.packages[name];
final upgradable = upgradablePackages.firstWhereOrNull(
(id) => id.name == name,
);
final resolvable = resolvablePackages.firstWhereOrNull(
(id) => id.name == name,
);
// Find the latest version, and if it's overridden.
var latestIsOverridden = false;
PackageId? latest;
// If not overridden in current resolution we can use this
if (!hasOverride(entrypoint.workspaceRoot, name)) {
latest ??= await cache.getLatest(
current?.toRef(),
version: current?.version,
allowPrereleases: prereleases,
);
}
// If present as a dependency or dev_dependency we use this
latest ??= await cache.getLatest(
allDependencies(
baseWorkspace,
).firstWhereOrNull((r) => r.name == name)?.toRef(),
allowPrereleases: prereleases,
);
latest ??= await cache.getLatest(
allDevDependencies(
baseWorkspace,
).firstWhereOrNull((r) => r.name == name)?.toRef(),
allowPrereleases: prereleases,
);
// If not overridden and present in either upgradable or resolvable we
// use this reference to find the latest
if (!hasOverride(upgradableWorkspace, name)) {
latest ??= await cache.getLatest(
upgradable?.toRef(),
version: upgradable?.version,
allowPrereleases: prereleases,
);
}
if (!hasOverride(resolvableWorkspace, name)) {
latest ??= await cache.getLatest(
resolvable?.toRef(),
version: resolvable?.version,
allowPrereleases: prereleases,
);
}
// Otherwise, we might simply not have a latest, when a transitive
// dependency is overridden the source can depend on which versions we
// are picking. This is not a problem on `pub.dev` because it does not
// allow 3rd party pub servers, but other servers might. Hence, we choose
// to fallback to using the overridden source for latest.
if (latest == null) {
final id = current ?? upgradable ?? resolvable;
latest ??= await cache.getLatest(
id?.toRef(),
version: id?.version,
allowPrereleases: prereleases,
);
latestIsOverridden = true;
}
final currentStatus = await current?.source.status(
current.toRef(),
current.version,
cache,
);
final id = current ?? upgradable ?? resolvable ?? latest;
var packageAdvisories =
await id?.source.getAdvisoriesForPackage(
id,
cache,
const Duration(days: 3),
) ??
[];
final discontinued =
currentStatus == null ? false : currentStatus.isDiscontinued;
final discontinuedReplacedBy = currentStatus?.discontinuedReplacedBy;
final isCurrentRetracted =
currentStatus == null ? false : currentStatus.isRetracted;
final currentVersionDetails = await _describeVersion(
current,
entrypoint.workspaceRoot.pubspec.dependencyOverrides.containsKey(name),
);
final upgradableVersionDetails = await _describeVersion(
upgradable,
hasOverride(upgradableWorkspace, name),
);
final resolvableVersionDetails = await _describeVersion(
resolvable,
hasOverride(resolvableWorkspace, name),
);
final latestVersionDetails = await _describeVersion(
latest,
latestIsOverridden,
);
final isLatest = currentVersionDetails == latestVersionDetails;
var isCurrentAffectedByAdvisory = false;
if (currentVersionDetails != null) {
// Filter out advisories added to `ignored_advisores` in the root
// pubspec.
packageAdvisories =
packageAdvisories
.where(
(adv) =>
entrypoint.workspaceRoot.pubspec.ignoredAdvisories
.intersection({...adv.aliases, adv.id})
.isEmpty,
)
.toList();
for (final advisory in packageAdvisories) {
if (advisory.affectedVersions.contains(
currentVersionDetails._pubspec.version.canonicalizedVersion,
)) {
isCurrentAffectedByAdvisory = true;
}
}
}
return _PackageDetails(
name: name,
current: currentVersionDetails,
upgradable: upgradableVersionDetails,
resolvable: resolvableVersionDetails,
latest: latestVersionDetails,
kind: _kind(name, entrypoint, nonDevDependencies),
isDiscontinued: discontinued,
discontinuedReplacedBy: discontinuedReplacedBy,
isCurrentRetracted: isCurrentRetracted,
isLatest: isLatest,
advisories: packageAdvisories,
isCurrentAffectedBySecurityAdvisory: isCurrentAffectedByAdvisory,
);
}
final rows = <_PackageDetails>[];
final visited = {
...entrypoint.workspaceRoot.transitiveWorkspace.map(
(package) => package.name,
),
};
// Add all dependencies from the lockfile.
for (final id in [
...currentPackages,
...upgradablePackages,
...resolvablePackages,
]) {
if (!visited.add(id.name)) continue;
rows.add(await analyzeDependency(id.toRef()));
}
if (!includeDevDependencies) {
rows.removeWhere((r) => r.kind == _DependencyKind.dev);
}
rows.sort();
final showAll =
argResults.flag('show-all') || argResults.flag('up-to-date');
if (argResults.flag('json')) {
await _outputJson(
rows,
mode,
showAll: showAll,
includeDevDependencies: includeDevDependencies,
);
} else {
bool isNotFromSdk(PackageRange range) => range.source is! SdkSource;
await _outputHuman(
rows,
mode,
useColors: canUseAnsiCodes,
showAll: showAll,
includeDevDependencies: includeDevDependencies,
lockFileExists: fileExists(entrypoint.lockFilePath),
hasDirectDependencies: allDependencies(baseWorkspace).any(
// Test if it contains non-SDK dependencies
isNotFromSdk,
),
hasDevDependencies: allDevDependencies(baseWorkspace).any(
// Test if it contains non-SDK dependencies
isNotFromSdk,
),
showTransitiveDependencies: showTransitiveDependencies,
hasUpgradableResolution: hasUpgradableResolution,
hasResolvableResolution: hasResolvableResolution,
directory: p.normalize(directory),
);
}
}
bool get showTransitiveDependencies {
return argResults.flag('transitive');
}
late final bool prereleases = () {
// First check if 'prereleases' was passed as an argument.
// If that was not the case, check for use of the legacy spelling
// 'pre-releases'.
// Otherwise fall back to the default implied by the mode.
if (argResults.wasParsed('prereleases')) {
return argResults.flag('prereleases');
}
if (argResults.wasParsed('pre-releases')) {
return argResults.flag('pre-releases');
}
return false;
}();
/// Retrieves the pubspec of package [id] from its [PackageId.source].
///
/// Returns `null`, if given `null` as a convinience.
Future<_VersionDetails?> _describeVersion(
PackageId? id,
bool isOverridden,
) async {
if (id == null) {
return null;
}
return _VersionDetails(await cache.describe(id), id, isOverridden);
}
/// Computes the closure of the graph of dependencies (not including
/// `dev_dependencies`) from all workspace packages in [workspaceRoot], given
/// the package versions in [resolution].
///
/// The [resolution] is allowed to be a partial (or empty) resolution not
/// satisfying all the dependencies of [workspaceRoot].
Future<Set<String>> _nonDevDependencyClosure(
Package workspaceRoot,
Iterable<PackageId> resolution,
) async {
final nameToId = {for (final id in resolution) id.name: id};
final result = <String>{
for (final p in workspaceRoot.transitiveWorkspace) p.name,
};
final queue = [
for (final p in workspaceRoot.transitiveWorkspace) ...p.dependencies.keys,
];
while (queue.isNotEmpty) {
final name = queue.removeLast();
if (!result.add(name)) {
continue;
}
final id = nameToId[name];
if (id == null) {
continue; // allow partial resolutions
}
final pubspec = await cache.describe(id);
queue.addAll(pubspec.dependencies.keys);
}
return result;
}
}
/// Try to resolve the pubspec of [package] return [PackageId]s in the
/// resolution or `null` if no resolution was found.
Future<List<PackageId>?> _tryResolve(
Package package,
SystemCache cache, {
LockFile? lockFile,
}) async {
final solveResult = await tryResolveVersions(
SolveType.upgrade,
cache,
package,
lockFile: lockFile,
);
return solveResult?.packages;
}
Future<void> _outputJson(
List<_PackageDetails> rows,
_Mode mode, {
required bool showAll,
required bool includeDevDependencies,
}) async {
final markedRows = Map.fromIterables(
rows,
await mode.markVersionDetails(rows),
);
if (!showAll) {
rows.removeWhere((row) => row.isLatest);
}
if (!includeDevDependencies) {
rows.removeWhere(
(element) =>
element.kind == _DependencyKind.dev ||
element.kind == _DependencyKind.devTransitive,
);
}
String kindString(_DependencyKind kind) {
return {
_DependencyKind.direct: 'direct',
_DependencyKind.dev: 'dev',
}[kind] ??
'transitive';
}
log.message(
const JsonEncoder.withIndent(' ').convert({
'packages': [
...(rows..sort((a, b) => a.name.compareTo(b.name))).map(
(packageDetails) => {
'package': packageDetails.name,
'kind': kindString(packageDetails.kind),
'isDiscontinued': packageDetails.isDiscontinued,
'isCurrentRetracted': packageDetails.isCurrentRetracted,
'isCurrentAffectedByAdvisory':
packageDetails.isCurrentAffectedBySecurityAdvisory,
'current': markedRows[packageDetails]![0].toJson(),
'upgradable': markedRows[packageDetails]![1].toJson(),
'resolvable': markedRows[packageDetails]![2].toJson(),
'latest': markedRows[packageDetails]![3].toJson(),
},
),
],
}),
);
}
Future<void> _outputHuman(
List<_PackageDetails> rows,
_Mode mode, {
required bool showAll,
required bool useColors,
required bool includeDevDependencies,
required bool lockFileExists,
required bool hasDirectDependencies,
required bool hasDevDependencies,
required bool showTransitiveDependencies,
required bool hasUpgradableResolution,
required bool hasResolvableResolution,
required String directory,
}) async {
final directoryDesc = directory == '.' ? '' : ' in $directory';
log.message('${mode.explanation(directoryDesc)}\n');
final markedRows = Map.fromIterables(
rows,
await mode.markVersionDetails(rows),
);
List<FormattedString> formatted(_PackageDetails package) => [
FormattedString(package.name),
...markedRows[package]!.map((m) => m.toHuman()),
];
if (!showAll) {
rows.removeWhere((row) => row.isLatest);
}
if (rows.isEmpty) {
log.message(mode.foundNoBadText);
return;
}
bool Function(_PackageDetails) hasKind(_DependencyKind kind) =>
(row) => row.kind == kind;
final directRows = rows.where(hasKind(_DependencyKind.direct)).map(formatted);
final devRows = rows.where(hasKind(_DependencyKind.dev)).map(formatted);
final transitiveRows = rows
.where(hasKind(_DependencyKind.transitive))
.map(formatted);
final devTransitiveRows = rows
.where(hasKind(_DependencyKind.devTransitive))
.map(formatted);
final formattedRows = <List<FormattedString>>[
[
'Package Name',
'Current',
'Upgradable',
'Resolvable',
'Latest',
].map((s) => format(s, log.bold)).toList(),
if (hasDirectDependencies) ...[
[
if (directRows.isEmpty)
format('\ndirect dependencies: ${mode.allGood}', log.bold)
else
format('\ndirect dependencies:', log.bold),
],
...directRows,
],
if (includeDevDependencies && hasDevDependencies) ...[
[
if (devRows.isEmpty)
format('\ndev_dependencies: ${mode.allGood}', log.bold)
else
format('\ndev_dependencies:', log.bold),
],
...devRows,
],
if (showTransitiveDependencies) ...[
if (transitiveRows.isNotEmpty)
[format('\ntransitive dependencies:', log.bold)],
...transitiveRows,
if (includeDevDependencies) ...[
if (devTransitiveRows.isNotEmpty)
[format('\ntransitive dev_dependencies:', log.bold)],
...devTransitiveRows,
],
],
];
for (final line in log.renderTable(formattedRows, useColors)) {
log.message(line);
}
final upgradable =
rows.where((row) {
final current = row.current;
final upgradable = row.upgradable;
return current != null &&
upgradable != null &&
current < upgradable &&
// Include transitive only, if we show them
(showTransitiveDependencies ||
hasKind(_DependencyKind.direct)(row) ||
hasKind(_DependencyKind.dev)(row));
}).length;
final notAtResolvable =
rows.where((row) {
final current = row.current;
final upgradable = row.upgradable;
final resolvable = row.resolvable;
return (current != null || !lockFileExists) &&
resolvable != null &&
upgradable != null &&
upgradable < resolvable &&
// Include transitive only, if we show them
(showTransitiveDependencies ||
hasKind(_DependencyKind.direct)(row) ||
hasKind(_DependencyKind.dev)(row));
}).length;
if (!hasUpgradableResolution || !hasResolvableResolution) {
log.message(mode.noResolutionText);
} else if (lockFileExists) {
if (upgradable != 0) {
if (upgradable == 1) {
log.message(
'\n1 upgradable dependency is locked (in pubspec.lock) to '
'an older version.\n'
'To update it, use `$topLevelProgram pub upgrade`.',
);
} else {
log.message(
'\n$upgradable upgradable dependencies are locked '
'(in pubspec.lock) to older versions.\n'
'To update these dependencies, use `$topLevelProgram pub upgrade`.',
);
}
}
if (notAtResolvable == 0 &&
upgradable == 0 &&
rows.isNotEmpty &&
(directRows.isNotEmpty || devRows.isNotEmpty)) {
log.message(
'You are already using the newest resolvable versions listed in the '
"'Resolvable' column.\n"
"Newer versions, listed in 'Latest', may not be mutually compatible.",
);
} else if (directRows.isEmpty && devRows.isEmpty) {
log.message(mode.allSafe);
}
} else {
log.message(
'\nNo pubspec.lock found. There are no Current versions.\n'
'Run `$topLevelProgram pub get` to create a pubspec.lock '
'with versions matching your '
'pubspec.yaml.',
);
}
if (notAtResolvable != 0) {
if (notAtResolvable == 1) {
log.message(
'\n1 dependency is constrained to a '
'version that is older than a resolvable version.\n'
'To update it, ${mode.upgradeConstrained}.',
);
} else {
log.message(
'\n$notAtResolvable dependencies are constrained to '
'versions that are older than a resolvable version.\n'
'To update these dependencies, ${mode.upgradeConstrained}.',
);
}
}
List<Advisory> advisoriesWithAffectedVersions(_PackageDetails package) {
return package.advisories
.where(
(advisory) =>
advisory.affectedVersions
.intersection(
[
package.current,
package.upgradable,
package.resolvable,
package.latest,
]
.map((e) => e?._pubspec.version.canonicalizedVersion)
.toSet(),
)
.isNotEmpty,
)
.toList();
}
final advisoriesToDisplay = <String, List<Advisory>>{};
for (final package in rows) {
advisoriesToDisplay[package.name] = advisoriesWithAffectedVersions(package);
}
bool displayExtraInfo(_PackageDetails package) =>
package.isDiscontinued ||
package.isCurrentRetracted ||
(advisoriesToDisplay[package.name]!.isNotEmpty);
if (rows.any(displayExtraInfo)) {
log.message('\n');
for (var package in rows.where(displayExtraInfo)) {
log.message(log.bold(package.name));
if (package.isDiscontinued) {
final replacedByText =
package.discontinuedReplacedBy != null
? ', replaced by ${package.discontinuedReplacedBy}.'
: '.';
log.message(
' Package ${package.name} has been discontinued$replacedByText '
'See https://dart.dev/go/package-discontinue',
);
}
if (package.isCurrentRetracted) {
log.message(
' Version ${package.current!._id.version} is retracted. '
'See https://dart.dev/go/package-retraction',
);
}
final displayedAdvisories = advisoriesToDisplay[package.name]!;
if (displayedAdvisories.isNotEmpty) {
final advisoriesText =
displayedAdvisories.length > 1
? 'security advisories'
: 'a security advisory';
log.message(
' Package ${package.name} is affected by $advisoriesText. '
'See https://dart.dev//go/pub-security-advisories',
);
log.message('\n');
for (final advisory in displayedAdvisories) {
final displayedVersions = advisory.affectedVersions.intersection(
[
package.current,
package.upgradable,
package.resolvable,
package.latest,
].map((e) => e?._pubspec.version.canonicalizedVersion).toSet(),
);
log.message(' - "${advisory.summary}"');
log.message(' Affects: ${displayedVersions.join(', ')}');
log.message(' ${advisory.displayHandle}');
}
}
}
}
}
abstract class _Mode {
/// Analyzes the [_PackageDetails] according to a --mode and outputs a
/// corresponding list of the versions
/// [current, upgradable, resolvable, latest].
Future<List<List<_Details>>> markVersionDetails(
List<_PackageDetails> packageDetails,
);
String explanation(String directoryDescription);
String get foundNoBadText;
String get allGood;
String get noResolutionText;
String get upgradeConstrained;
String get allSafe;
Pubspec resolvablePubspec(Pubspec pubspec);
}
class _OutdatedMode implements _Mode {
@override
String explanation(String directoryDescription) => '''
Showing outdated packages$directoryDescription.
[${log.red('*')}] indicates versions that are not the latest available.
''';
@override
String get foundNoBadText => 'Found no outdated packages';
@override
String get allGood => 'all up-to-date.';
@override
String get noResolutionText =>
'''No resolution was found. Try running `$topLevelProgram pub upgrade --dry-run` to explore why.''';
@override
String get upgradeConstrained =>
'edit pubspec.yaml, or run '
'`$topLevelProgram pub upgrade --major-versions`';
@override
String get allSafe => 'all dependencies are up-to-date.';
@override
Future<List<List<_Details>>> markVersionDetails(
List<_PackageDetails> packages,
) async {
final rows = <List<_Details>>[];
for (final packageDetails in packages) {
final cols = <_Details>[];
_VersionDetails? previous;
for (final versionDetails in [
packageDetails.current,
packageDetails.upgradable,
packageDetails.resolvable,
packageDetails.latest,
]) {
String Function(String)? color;
String? prefix;
String? suffix;
if (versionDetails != null) {
final isLatest = versionDetails == packageDetails.latest;
final isCurrent = versionDetails == packageDetails.current;
if (isLatest) {
color = versionDetails == previous ? color = log.gray : null;
} else {
color = log.red;
if (isCurrent) {
if (packageDetails.isCurrentRetracted) {
suffix = ' (retracted)';
}
}
}
final advisories = packageDetails.advisories;
final hasAdvisory =
advisories
.where(
(advisory) => advisory.affectedVersions.contains(
versionDetails._pubspec.version.canonicalizedVersion,
),
)
.isNotEmpty;
if (hasAdvisory) {
suffix = '${suffix ?? ''} (advisory)';
}
prefix = isLatest ? '' : '*';
}
cols.add(
_MarkedVersionDetails(
versionDetails,
format: color,
prefix: prefix,
suffix: suffix,
),
);
previous = versionDetails;
}
if (packageDetails.isDiscontinued == true) {
cols.add(_SimpleDetails('(discontinued)'));
}
rows.add(cols);
}
return rows;
}
@override
Pubspec resolvablePubspec(Pubspec pubspec) {
return stripVersionBounds(pubspec);
}
}
/// Details about a single version of a package.
class _VersionDetails {
final Pubspec _pubspec;
/// True if this version is overridden.
final bool _overridden;
final PackageId _id;
_VersionDetails(this._pubspec, this._id, this._overridden);
/// A string representation of this version to include in the outdated report.
String get describe {
final version = _pubspec.version;
var suffix = '';
if (_overridden) {
suffix = ' (overridden)';
} else if (_id.source is SdkSource) {
// Version is not relevant for sdk-packages.
return '(sdk)';
} else if (_id.source is GitSource) {
suffix = ' (git)';
} else if (_id.source is PathSource) {
suffix = ' (path)';
}
return '$version$suffix';
}
Map<String, Object> toJson() => {
'version': _pubspec.version.toString(),
if (_overridden) 'overridden': true,
};
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is _VersionDetails &&
_overridden == other._overridden &&
_id.source == other._id.source &&
_pubspec.version == other._pubspec.version;
bool operator <(_VersionDetails other) =>
_overridden == other._overridden &&
_id.source == other._id.source &&
_pubspec.version < other._pubspec.version;
@override
int get hashCode => Object.hash(_pubspec.version, _id.source, _overridden);
}
class _PackageDetails implements Comparable<_PackageDetails> {
final String name;
final _VersionDetails? current;
final _VersionDetails? upgradable;
final _VersionDetails? resolvable;
final _VersionDetails? latest;
final _DependencyKind kind;
final bool isDiscontinued;
final String? discontinuedReplacedBy;
final bool isCurrentRetracted;
final bool isLatest;
/// List of advisories affecting this package which are not present in the
/// `ignored_advisories` list in the pubspec.
final List<Advisory> advisories;
final bool isCurrentAffectedBySecurityAdvisory;
_PackageDetails({
required this.name,
required this.current,
required this.upgradable,
required this.resolvable,
required this.latest,
required this.kind,
required this.isDiscontinued,
required this.discontinuedReplacedBy,
required this.isCurrentRetracted,
required this.isLatest,
required this.advisories,
required this.isCurrentAffectedBySecurityAdvisory,
});
@override
int compareTo(_PackageDetails other) {
if (kind != other.kind) {
return kind.index.compareTo(other.kind.index);
}
return name.compareTo(other.name);
}
}
_DependencyKind _kind(
String name,
Entrypoint entrypoint,
Set<String> nonDevTransitive,
) {
if (hasDependency(entrypoint.workspaceRoot, name)) {
return _DependencyKind.direct;
} else if (hasDevDependency(entrypoint.workspaceRoot, name)) {
return _DependencyKind.dev;
} else {
if (nonDevTransitive.contains(name)) {
return _DependencyKind.transitive;
} else {
return _DependencyKind.devTransitive;
}
}
}
enum _DependencyKind {
/// Direct non-dev dependencies.
direct,
/// Direct dev dependencies.
dev,
/// Transitive dependencies of direct dependencies.
transitive,
/// Transitive dependencies needed only by dev_dependencies.
devTransitive,
}
abstract class _Details {
FormattedString toHuman();
Object? toJson();
}
class _SimpleDetails implements _Details {
final String details;
_SimpleDetails(this.details);
@override
FormattedString toHuman() => FormattedString(details);
@override
Object? toJson() => null;
}
class _MarkedVersionDetails implements _Details {
final MapEntry<String, Object>? _jsonExplanation;
final _VersionDetails? _versionDetails;
final String Function(String)? _format;
final String? _prefix;
final String? _suffix;
_MarkedVersionDetails(
this._versionDetails, {
String Function(String)? format,
String? prefix = '',
String? suffix = '',
MapEntry<String, Object>? jsonExplanation,
}) : _format = format,
_prefix = prefix,
_suffix = suffix,
_jsonExplanation = jsonExplanation;
@override
FormattedString toHuman() => FormattedString(
_versionDetails?.describe ?? '-',
format: _format,
prefix: _prefix,
suffix: _suffix,
);
@override
Object? toJson() {
if (_versionDetails == null) return null;
final jsonExplanation = _jsonExplanation;
return jsonExplanation == null
? _versionDetails.toJson()
: (_versionDetails.toJson()..addEntries([jsonExplanation]));
}
}
/// Whether the package [name] is overridden anywhere in the workspace rooted at
/// [workspaceRoot].
bool hasOverride(Package workspaceRoot, String name) {
return workspaceRoot.allOverridesInWorkspace.containsKey(name);
}
/// Whether the package [name] is depended on directly anywhere in the workspace
/// rooted at [workspaceRoot].
bool hasDependency(Package workspaceRoot, String name) {
return workspaceRoot.transitiveWorkspace.any(
(p) => p.dependencies.containsKey(name),
);
}
/// Whether the package [name] is dev-depended on directly anywhere in the
/// workspace rooted at [workspaceRoot].
bool hasDevDependency(Package workspaceRoot, String name) {
return workspaceRoot.transitiveWorkspace.any(
(p) => p.devDependencies.containsKey(name),
);
}
Iterable<PackageRange> allDependencies(Package workspaceRoot) =>
workspaceRoot.transitiveWorkspace.expand((p) => p.dependencies.values);
Iterable<PackageRange> allDevDependencies(Package workspaceRoot) =>
workspaceRoot.transitiveWorkspace.expand((p) => p.devDependencies.values);