blob: a25345749d94265be72388be1b83e67fd618e3ef [file]
// Copyright (c) 2026, 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:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/context/packages.dart';
import 'package:analyzer/src/hint/sdk_constraint_extractor.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:pub_semver/pub_semver.dart';
import 'package:yaml/yaml.dart';
/// Checks if [targetVersion] is compatible with the SDK constraints of all
/// resolved [packages].
///
/// Returns a list of package names that are incompatible.
List<String> checkDependencyCompatibility({
required Iterable<Package> packages,
required Version targetVersion,
}) {
var incompatible = <String>[];
for (var package in packages) {
var constraint = packageSdkConstraint(package.rootFolder);
if (constraint == null) continue;
// A dependency is incompatible only if its upper bound forbids all versions
// at or above [targetVersion]. Dependencies that require an even higher
// minimum SDK version remain compatible with this bump.
if (!constraint.allowsAny(VersionRange(min: targetVersion))) {
incompatible.add(package.name);
}
}
return incompatible;
}
/// Calculates the edit to update the SDK constraint in [pubspecFile] to the
/// given [minimumVersion].
///
/// Returns `null` if the constraint cannot be found or is not in a supported
/// format.
PubspecEdit? computeEdit(File pubspecFile, Version minimumVersion) {
var extractor = SdkConstraintExtractor(pubspecFile);
var text = extractor.constraintText();
var offset = extractor.constraintOffset();
if (text == null || offset < 0) {
return null;
}
String newConstraint;
if (text == 'any' || text.startsWith('^')) {
newConstraint = '^$minimumVersion';
} else if (text.startsWith('>')) {
var constraint = extractor.constraint();
if (constraint is! VersionRange) return null;
/// If the [constraint] allows no version at or above [minimumVersion],
/// replace the whole constraint. e.g., `>=3.12.0 <3.13.0` allows nothing from
/// 3.13.0 up, so raising its lower bound to 3.13.0 would make it
/// unsatisfiable.
if (!constraint.allowsAny(
VersionRange(min: minimumVersion, includeMin: true),
)) {
newConstraint = '^$minimumVersion';
} else {
// Raise the lower bound and keep the upper bound, whatever whitespace
// the original constraint was written with.
newConstraint = VersionRange(
min: minimumVersion,
includeMin: true,
max: constraint.max,
includeMax: constraint.includeMax,
).toString();
}
} else {
return null;
}
return PubspecEdit(
offset: offset,
originalConstraint: text,
newConstraint: newConstraint,
targetVersion: minimumVersion,
);
}
/// Returns the minimum SDK version constraint defined in [pubspecFile], or
/// `null` if the constraint cannot be found or parsed.
Version? minimumSdkConstraint(File pubspecFile) {
var extractor = SdkConstraintExtractor(pubspecFile);
var constraint = extractor.constraint();
if (constraint is VersionRange) {
return constraint.min;
} else if (constraint is Version) {
return constraint;
}
return null;
}
/// Returns the SDK constraint declared by the package rooted at [packageRoot],
/// or `null` if it doesn't declare one that can be checked against.
///
/// The constraint of a null-safe package whose upper bound is `<3.0.0` is
/// re-interpreted as `<4.0.0`, matching how such a package is resolved by pub.
/// See
/// https://dart.dev/resources/dart-3-migration#dart-3-backwards-compatibility.
VersionConstraint? packageSdkConstraint(Folder packageRoot) {
var pubspecFile = packageRoot.getFile(file_paths.pubspecYaml);
if (!pubspecFile.exists) return null;
var constraint = SdkConstraintExtractor(pubspecFile).constraint();
if (constraint is! VersionRange) return constraint;
var min = constraint.min;
var nullSafetyVersion = Version(2, 12, 0).firstPreRelease;
var dart3Version = Version(3, 0, 0).firstPreRelease;
if (min != null &&
min >= nullSafetyVersion &&
constraint.max == dart3Version &&
!constraint.includeMax) {
return VersionRange(
min: min,
includeMin: constraint.includeMin,
max: Version(4, 0, 0),
);
}
return constraint;
}
/// The result of computing an edit to a pubspec file's SDK constraint.
///
/// The edit replaces the whole of [originalConstraint] with [newConstraint].
class PubspecEdit({
/// The character offset in the document where the edit should be applied.
required final int offset,
/// The full original SDK constraint text before the edit is applied.
required final String originalConstraint,
/// The full new SDK constraint text after the edit is applied.
required final String newConstraint,
/// The target version to migrate to.
required final Version targetVersion,
}) {
/// The length of the text to be replaced.
final int length = originalConstraint.length;
}
/// A target package's `pubspec.yaml` file and the parts of it a migration
/// needs.
///
/// Used to avoid reading and parsing the `pubspec.yaml` file multiple times.
class PubspecTarget {
/// The `pubspec.yaml` file for the package.
final File file;
/// The name the package declares, or `null` if it doesn't declare one.
///
/// This is the name a dependent writes in [dependencyNames] to refer to this
/// package, so it is what identifies the package to anything reasoning about
/// dependencies. Use [displayName] to name the package in output instead: a
/// package without a declared name still has to be reported somehow.
final String? name;
/// The names of the packages this one depends on, directly or transitively.
///
/// Dev dependencies are included: they take part in the version solve for
/// the package that declares them, so they're subject to the same SDK
/// constraint ordering as regular dependencies.
///
/// Transitive dependencies are only known when the package has been
/// resolved. They include `dependency_overrides`.
final Set<String> dependencyNames;
/// Creates a target from the parsed [pubspec] in [file].
///
/// The [resolvedPackages] are the packages in the package config the package
/// is analyzed with. That config can belong to a parent directory and list
/// packages this one doesn't depend on, which only holds back more than
/// needed.
new({
required this.file,
required YamlMap pubspec,
required Packages resolvedPackages,
}) : name = switch (pubspec['name']) {
String name => name,
_ => null,
},
dependencyNames = _dependencyNamesIn(pubspec, resolvedPackages);
/// The name to show for the package, falling back to the directory it sits
/// in when it doesn't declare one.
String get displayName => name ?? file.parent.shortName;
static Set<String> _dependencyNamesIn(
YamlMap pubspec,
Packages resolvedPackages,
) => {
for (var section in const ['dependencies', 'dev_dependencies'])
if (pubspec[section] case YamlMap dependencies)
...dependencies.keys.whereType<String>(),
for (var package in resolvedPackages.packages) package.name,
};
}