blob: 9bc590b9d1b5af1c6384f13085a894bdb69d8aa9 [file] [edit]
// 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.
// This file contains logic for analyzing the subdirectories of the SDK's `pkg`
// directory, and producing metadata about those packages, such as:
// - Which team owns each package
// - Whether the package is published to pub.
//
// This information can be used for automated refactoring of large parts of the
// SDK codebase.
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:analyzer_testing/package_root.dart' as package_root;
import 'package:collection/collection.dart';
import 'package:path/path.dart' as p;
import 'package:yaml/yaml.dart';
final List<PackageInfo> packages = _computePackages();
/// The absolute path of the root of the SDK repository (the directory
/// containing `pkg`).
final String repoRoot = p.dirname(p.normalize(package_root.packageRoot));
List<PackageInfo> _computePackages() {
var result = <PackageInfo>[];
for (var entity in Directory(
package_root.packageRoot,
).listSync().whereType<Directory>()) {
var packageName = p.basename(entity.path);
var pubspecPath = p.join(entity.path, 'pubspec.yaml');
var pubspecFile = File(pubspecPath);
if (!pubspecFile.existsSync()) continue;
var pubspec = loadYaml(
pubspecFile.readAsStringSync(),
sourceUrl: Uri.file(pubspecPath),
);
if (pubspec is! YamlMap) {
throw StateError('Pubspec $pubspecPath is not a map');
}
if (pubspec['name'] case var pubspecName when pubspecName != packageName) {
throw StateError(
'Pubspec $pubspecPath says the package name is '
'${json.encode(pubspecName)}; expected ${json.encode(packageName)}',
);
}
var isPublished = pubspec['publish_to'] != 'none';
var sdkConstraint =
(pubspec['environment'] as YamlMap).nodes['sdk'] as YamlScalar;
var ownerStrings = _resolveOwnerStrings(p.join(entity.path, 'OWNERS'));
result.add(
PackageInfo(
path: entity.path,
name: packageName,
isPublished: isPublished,
ownerSet: OwnerSet.fromStrings(ownerStrings),
sdkConstraint: sdkConstraint.value as String,
sdkConstraintNode: sdkConstraint,
),
);
}
// `listSync` returns directories in whatever order the file system does, so
// sort to keep everything derived from this list deterministic.
return result..sortBy((package) => package.name);
}
/// Resolves the set of teams that own the package whose `OWNERS` file is
/// located at [ownersPath].
///
/// Team ownership is expressed by a `file:` directive referring to one of the
/// `/tools/OWNERS_*` files; the returned strings are the lowercased suffixes of
/// those file names (so `file:/tools/OWNERS_MODEL` yields `model`).
///
/// Two other forms of `file:` directive are recognized:
/// - A reference to the repository-global `/OWNERS` file is ignored, since it
/// applies to every package and therefore says nothing about which team owns
/// this particular one.
/// - A reference to another package's `OWNERS` file (for example,
/// `pkg/dart_runtime_service_dds` defers to `pkg/dds`) is resolved
/// recursively, so that the referring package inherits the referenced
/// package's owners.
///
/// `per-file` directives are deliberately not considered, because they describe
/// ownership of individual files rather than of the package as a whole.
///
/// [visited] guards against cycles among mutually-referential `OWNERS` files.
List<String> _resolveOwnerStrings(String ownersPath, {Set<String>? visited}) {
visited ??= {};
if (!visited.add(p.canonicalize(ownersPath))) {
// Already resolved (or currently being resolved) via another path.
return const [];
}
var ownersFile = File(ownersPath);
if (!ownersFile.existsSync()) return const [];
var ownerStrings = <String>[];
for (var line in ownersFile.readAsStringSync().split('\n')) {
line = line.trim();
const filePrefix = 'file:';
if (!line.startsWith(filePrefix)) continue;
var referencedPath = line.substring(filePrefix.length);
// Repository-root-relative references may be written with either one or
// two leading slashes; normalize to one.
if (referencedPath.startsWith('//')) {
referencedPath = referencedPath.substring(1);
}
if (referencedPath == '/OWNERS') continue;
const ownersFilePrefix = '/tools/OWNERS_';
if (referencedPath.startsWith(ownersFilePrefix)) {
ownerStrings.add(
referencedPath.substring(ownersFilePrefix.length).toLowerCase(),
);
} else if (p.basename(referencedPath) == 'OWNERS') {
ownerStrings.addAll(
_resolveOwnerStrings(
p.join(repoRoot, referencedPath.substring(1)),
visited: visited,
),
);
} else {
throw StateError(
'OWNERS file $ownersPath refers to unexpected file $referencedPath',
);
}
}
return ownerStrings;
}
/// The set of teams that own a package, as computed by [_resolveOwnerStrings]:
/// the lowercased suffixes of the `/tools/OWNERS_*` files that the package's
/// `OWNERS` file refers to, directly or via other packages' `OWNERS` files.
///
/// This class provides an `==` operator and `hashCode` implementation, so that
/// packages can be easily categorized by their `OwnerSet`. Packages are
/// compared by their whole owner set: a package owned by `model` and
/// `developer_experience` has a different owner set from one owned by `model`
/// alone. That's deliberate, so that each migration commit has a single set of
/// owners to review it.
class OwnerSet with SetMixin<String> {
final Set<String> _owners;
new fromStrings(Iterable<String> strings) : _owners = strings.toSet();
@override
int get hashCode => const SetEquality<String>().hash(_owners);
@override
Iterator<String> get iterator => _owners.iterator;
@override
int get length => _owners.length;
@override
bool operator ==(Object other) =>
other is OwnerSet &&
const SetEquality<String>().equals(_owners, other._owners);
@override
bool add(String value) {
throw StateError('Do not modify OwnerSet');
}
@override
bool contains(Object? element) => _owners.contains(element);
@override
String? lookup(Object? element) => _owners.lookup(element);
@override
bool remove(Object? value) {
throw StateError('Do not modify OwnerSet');
}
@override
Set<String> toSet() => _owners.toSet();
@override
String toString() => '{${join(', ')}}';
}
class PackageInfo {
/// The absolute path of the package directory.
final String path;
/// The name of the package.
final String name;
/// Whether this package is published to `pub`.
final bool isPublished;
/// The set of teams that own this package. See [OwnerSet].
final OwnerSet ownerSet;
/// The current setting of the package's SDK constraint.
final String sdkConstraint;
/// The YAML node for the SDK constraint.
final YamlScalar sdkConstraintNode;
new({
required this.path,
required this.name,
required this.isPublished,
required this.ownerSet,
required this.sdkConstraint,
required this.sdkConstraintNode,
});
@override
String toString() =>
'PackageInfo(name: $name, isPublished: $isPublished, ownerSet: '
'$ownerSet)';
}