| // 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 '../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 '../path.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 an ANSI-capable | |
| /// terminal, and when we are outputting machine-readable json. | |
| bool get _shouldShowSpinner => | |
| terminalOutputForStdout && canUseAnsiCodes && !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.progress( | |
| '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, | |
| transient: true, | |
| ); | |
| // 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. | |
| final idsToAnalyze = <PackageRef>[]; | |
| for (final id in [ | |
| ...currentPackages, | |
| ...upgradablePackages, | |
| ...resolvablePackages, | |
| ]) { | |
| if (!visited.add(id.name)) continue; | |
| idsToAnalyze.add(id.toRef()); | |
| } | |
| await cache.prefetchAdvisoriesAndStatus([ | |
| ...currentPackages, | |
| ...upgradablePackages, | |
| ...resolvablePackages, | |
| ]); | |
| rows.addAll(await Future.wait(idsToAnalyze.map(analyzeDependency))); | |
| 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, | |
| 'replacedBy': packageDetails.discontinuedReplacedBy, | |
| '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) { | |
| // Link to pub.dev only if the package is hosted on pub.dev. | |
| final isFromPubDev = _isHostedOnPubDev(package); | |
| return [ | |
| FormattedString( | |
| package.name, | |
| format: isFromPubDev ? _linkToPubDev(package.name) : null, | |
| ), | |
| ...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}'); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| bool _isHostedOnPubDev(_PackageDetails package) { | |
| final details = | |
| package.latest ?? | |
| package.resolvable ?? | |
| package.upgradable ?? | |
| package.current; | |
| return details != null && | |
| details._id.source is HostedSource && | |
| HostedSource.isFromPubDev(details._id); | |
| } | |
| String Function(String) _linkToPubDev(String packageName) { | |
| return (String name) => | |
| log.link(name, 'https://pub.dev/packages/$packageName'); | |
| } | |
| 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); |