mirror of
https://github.com/dart-lang/sdk
synced 2024-09-05 00:13:50 +00:00
Add a tool to validate pubspec.yaml files in pkg/
Change-Id: If218f2aa48b58330cc506574d7dd68ad165a7b11 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/159162 Commit-Queue: Devon Carew <devoncarew@google.com> Reviewed-by: Jaime Wren <jwren@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
parent
c40edde709
commit
f7006a91bb
9
tools/package_deps/.gitignore
vendored
Normal file
9
tools/package_deps/.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
.packages
|
||||
|
||||
# Conventional directory for build outputs
|
||||
build/
|
||||
|
||||
# Directory created by dartdoc
|
||||
doc/api/
|
1
tools/package_deps/README.md
Normal file
1
tools/package_deps/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
A tool to validate pubspec files in pkg/.
|
353
tools/package_deps/bin/package_deps.dart
Normal file
353
tools/package_deps/bin/package_deps.dart
Normal file
|
@ -0,0 +1,353 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:yaml/yaml.dart' as yaml;
|
||||
|
||||
// TODO(devoncarew): Use bold ansi chars for emphasis.
|
||||
|
||||
// TODO(devoncarew): Validate that publishable packages don't use relative sdk
|
||||
// paths in their pubspecs.
|
||||
|
||||
// TODO(devoncarew): Find unused entries in the DEPS file.
|
||||
const validateDEPS = false;
|
||||
|
||||
void main(List<String> arguments) {
|
||||
// validate the cwd
|
||||
if (!FileSystemEntity.isFileSync('DEPS') ||
|
||||
!FileSystemEntity.isDirectorySync('pkg')) {
|
||||
print('Please run this tool from the root of the Dart repo.');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// TODO(devoncarew): Support manually added directories (outside of pkg/).
|
||||
|
||||
// locate all packages
|
||||
final packages = <Package>[];
|
||||
for (var entity in Directory('pkg').listSync()) {
|
||||
if (entity is Directory) {
|
||||
var package = Package(entity.path);
|
||||
if (package.hasPubspec) {
|
||||
packages.add(package);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packages.sort();
|
||||
|
||||
var validateFailure = false;
|
||||
|
||||
// For each, validate the pubspec contents.
|
||||
for (var package in packages) {
|
||||
print('validating ${package.dir}'
|
||||
'${package.publishable ? ' [publishable]' : ''}');
|
||||
|
||||
if (!package.validate()) {
|
||||
validateFailure = true;
|
||||
}
|
||||
|
||||
print('');
|
||||
}
|
||||
|
||||
// Read and display info about the sdk DEPS file.
|
||||
if (validateDEPS) {
|
||||
print('SDK DEPS');
|
||||
var sdkDeps = SdkDeps(File('DEPS'));
|
||||
sdkDeps.parse();
|
||||
print('');
|
||||
print('packages:');
|
||||
for (var pkg in sdkDeps.pkgs) {
|
||||
print(' package:$pkg');
|
||||
}
|
||||
|
||||
print('');
|
||||
print('tested packages:');
|
||||
for (var pkg in sdkDeps.testedPkgs) {
|
||||
print(' package:$pkg');
|
||||
}
|
||||
}
|
||||
|
||||
if (validateFailure) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
class Package implements Comparable<Package> {
|
||||
final String dir;
|
||||
|
||||
Package(this.dir) {
|
||||
_parsePubspec();
|
||||
}
|
||||
|
||||
String get dirName => path.basename(dir);
|
||||
final Set<String> _regularDependencies = {};
|
||||
final Set<String> _devDependencies = {};
|
||||
String _packageName;
|
||||
|
||||
String get packageName => _packageName;
|
||||
Set<String> _declaredDependencies;
|
||||
Set<String> _declaredDevDependencies;
|
||||
|
||||
List<String> get regularDependencies => _regularDependencies.toList()..sort();
|
||||
|
||||
List<String> get devDependencies => _devDependencies.toList()..sort();
|
||||
|
||||
bool _publishToNone;
|
||||
bool get publishable => !_publishToNone;
|
||||
|
||||
@override
|
||||
String toString() => 'Package $dirName';
|
||||
|
||||
bool get hasPubspec =>
|
||||
FileSystemEntity.isFileSync(path.join(dir, 'pubspec.yaml'));
|
||||
|
||||
@override
|
||||
int compareTo(Package other) {
|
||||
return dirName.compareTo(other.dirName);
|
||||
}
|
||||
|
||||
bool validate() {
|
||||
_parseImports();
|
||||
return _validatePubspecDeps();
|
||||
}
|
||||
|
||||
void _parseImports() {
|
||||
final files = <File>[];
|
||||
|
||||
_collectDartFiles(Directory(dir), files);
|
||||
|
||||
for (var file in files) {
|
||||
//print(' ${file.path}');
|
||||
|
||||
var importedPackages = <String>{};
|
||||
|
||||
for (var import in _collectImports(file)) {
|
||||
try {
|
||||
var uri = Uri.parse(import);
|
||||
if (uri.hasScheme && uri.scheme == 'package') {
|
||||
var packageName = path.split(uri.path).first;
|
||||
importedPackages.add(packageName);
|
||||
}
|
||||
} on FormatException {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
var topLevelDir = _topLevelDir(file);
|
||||
|
||||
if ({'bin', 'lib'}.contains(topLevelDir)) {
|
||||
_regularDependencies.addAll(importedPackages);
|
||||
} else {
|
||||
_devDependencies.addAll(importedPackages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _parsePubspec() {
|
||||
var pubspec = File(path.join(dir, 'pubspec.yaml'));
|
||||
var doc = yaml.loadYamlDocument(pubspec.readAsStringSync());
|
||||
dynamic docContents = doc.contents.value;
|
||||
_packageName = docContents['name'];
|
||||
_publishToNone = docContents['publish_to'] == 'none';
|
||||
|
||||
if (docContents['dependencies'] != null) {
|
||||
_declaredDependencies =
|
||||
Set<String>.from(docContents['dependencies'].keys);
|
||||
} else {
|
||||
_declaredDependencies = {};
|
||||
}
|
||||
if (docContents['dev_dependencies'] != null) {
|
||||
_declaredDevDependencies =
|
||||
Set<String>.from(docContents['dev_dependencies'].keys);
|
||||
} else {
|
||||
_declaredDevDependencies = {};
|
||||
}
|
||||
}
|
||||
|
||||
bool _validatePubspecDeps() {
|
||||
var fail = false;
|
||||
|
||||
if (dirName != packageName) {
|
||||
print(' Package name is different from the directory name.');
|
||||
fail = true;
|
||||
}
|
||||
|
||||
var deps = regularDependencies;
|
||||
deps.remove(packageName);
|
||||
|
||||
var devdeps = devDependencies;
|
||||
devdeps.remove(packageName);
|
||||
|
||||
// if (deps.isNotEmpty) {
|
||||
// print(' deps : ${deps}');
|
||||
// }
|
||||
// if (devdeps.isNotEmpty) {
|
||||
// print(' dev deps: ${devdeps}');
|
||||
// }
|
||||
|
||||
var undeclaredRegularUses = Set<String>.from(deps)
|
||||
..removeAll(_declaredDependencies);
|
||||
if (undeclaredRegularUses.isNotEmpty) {
|
||||
print(' ${_printSet(undeclaredRegularUses)} used in lib/ but not '
|
||||
"declared in 'dependencies:'.");
|
||||
fail = true;
|
||||
}
|
||||
|
||||
var undeclaredDevUses = Set<String>.from(devdeps)
|
||||
..removeAll(_declaredDependencies)
|
||||
..removeAll(_declaredDevDependencies);
|
||||
if (undeclaredDevUses.isNotEmpty) {
|
||||
print(' ${_printSet(undeclaredDevUses)} used in dev dirs but not '
|
||||
"declared in 'dev_dependencies:'.");
|
||||
fail = true;
|
||||
}
|
||||
|
||||
var extraRegularDeclarations = Set<String>.from(_declaredDependencies)
|
||||
..removeAll(deps);
|
||||
if (extraRegularDeclarations.isNotEmpty) {
|
||||
print(' ${_printSet(extraRegularDeclarations)} declared in '
|
||||
"'dependencies:' but not used in lib/.");
|
||||
fail = true;
|
||||
}
|
||||
|
||||
var extraDevDeclarations = Set<String>.from(_declaredDevDependencies)
|
||||
..removeAll(devdeps);
|
||||
// Remove package:pedantic as it is often declared as a dev dependency in
|
||||
// order to bring in its analysis_options.yaml file.
|
||||
extraDevDeclarations.remove('pedantic');
|
||||
if (extraDevDeclarations.isNotEmpty) {
|
||||
print(' ${_printSet(extraDevDeclarations)} declared in '
|
||||
"'dev_dependencies:' but not used in dev dirs.");
|
||||
fail = true;
|
||||
}
|
||||
|
||||
// Look for things declared in deps, not used in lib/, but that are used in
|
||||
// dev dirs.
|
||||
var misplacedDeps =
|
||||
extraRegularDeclarations.intersection(Set.from(devdeps));
|
||||
if (misplacedDeps.isNotEmpty) {
|
||||
print(" ${_printSet(misplacedDeps)} declared in 'dependencies:' but "
|
||||
'only used in dev dirs.');
|
||||
fail = true;
|
||||
}
|
||||
|
||||
if (!fail) {
|
||||
print(' No issues.');
|
||||
}
|
||||
|
||||
return !fail;
|
||||
}
|
||||
|
||||
void _collectDartFiles(Directory dir, List<File> files) {
|
||||
for (var entity in dir.listSync(followLinks: false)) {
|
||||
if (entity is Directory) {
|
||||
var name = path.basename(entity.path);
|
||||
|
||||
// Skip 'pkg/analyzer_cli/test/data'.
|
||||
// Skip 'pkg/front_end/test/id_testing/data/'.
|
||||
// Skip 'pkg/front_end/test/language_versioning/data/'.
|
||||
if (name == 'data' && path.split(entity.parent.path).contains('test')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip 'pkg/analysis_server/test/mock_packages'.
|
||||
if (name == 'mock_packages') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip 'pkg/front_end/testcases'.
|
||||
if (name == 'testcases') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!name.startsWith('.')) {
|
||||
_collectDartFiles(entity, files);
|
||||
}
|
||||
} else if (entity is File && entity.path.endsWith('.dart')) {
|
||||
files.add(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// look for both kinds of quotes
|
||||
static RegExp importRegex1 = RegExp(r"^(import|export)\s+\'(\S+)\'");
|
||||
static RegExp importRegex2 = RegExp(r'^(import|export)\s+"(\S+)"');
|
||||
|
||||
List<String> _collectImports(File file) {
|
||||
var results = <String>[];
|
||||
|
||||
for (var line in file.readAsLinesSync()) {
|
||||
// Check for a few tokens that should stop our parse.
|
||||
if (line.startsWith('class ') ||
|
||||
line.startsWith('typedef ') ||
|
||||
line.startsWith('mixin ') ||
|
||||
line.startsWith('enum ') ||
|
||||
line.startsWith('extension ') ||
|
||||
line.startsWith('void ') ||
|
||||
line.startsWith('Future ') ||
|
||||
line.startsWith('final ') ||
|
||||
line.startsWith('const ')) {
|
||||
break;
|
||||
}
|
||||
|
||||
var match = importRegex1.firstMatch(line);
|
||||
if (match != null) {
|
||||
results.add(match.group(2));
|
||||
continue;
|
||||
}
|
||||
|
||||
match = importRegex2.firstMatch(line);
|
||||
if (match != null) {
|
||||
results.add(match.group(2));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
String _topLevelDir(File file) {
|
||||
var relativePath = path.relative(file.path, from: dir);
|
||||
return path.split(relativePath).first;
|
||||
}
|
||||
}
|
||||
|
||||
String _printSet(Set<String> value) {
|
||||
var list = value.toList()..sort();
|
||||
list = list.map((item) => 'package:$item').toList();
|
||||
if (list.length > 1) {
|
||||
return list.sublist(0, list.length - 1).join(', ') + ' and ' + list.last;
|
||||
} else {
|
||||
return list.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
class SdkDeps {
|
||||
final File file;
|
||||
|
||||
List<String> pkgs = [];
|
||||
List<String> testedPkgs = [];
|
||||
|
||||
SdkDeps(this.file);
|
||||
|
||||
void parse() {
|
||||
// Var("dart_root") + "/third_party/pkg/dart2js_info":
|
||||
final pkgRegExp = RegExp(r'"/third_party/pkg/(\S+)"');
|
||||
|
||||
// Var("dart_root") + "/third_party/pkg_tested/dart_style":
|
||||
final testedPkgRegExp = RegExp(r'"/third_party/pkg_tested/(\S+)"');
|
||||
|
||||
for (var line in file.readAsLinesSync()) {
|
||||
var pkgDep = pkgRegExp.firstMatch(line);
|
||||
var testedPkgDep = testedPkgRegExp.firstMatch(line);
|
||||
|
||||
if (pkgDep != null) {
|
||||
pkgs.add(pkgDep.group(1));
|
||||
} else if (testedPkgDep != null) {
|
||||
testedPkgs.add(testedPkgDep.group(1));
|
||||
}
|
||||
}
|
||||
|
||||
pkgs.sort();
|
||||
testedPkgs.sort();
|
||||
}
|
||||
}
|
15
tools/package_deps/pubspec.yaml
Normal file
15
tools/package_deps/pubspec.yaml
Normal file
|
@ -0,0 +1,15 @@
|
|||
name: package_deps
|
||||
description: A tool to validate pubspec files in pkg/.
|
||||
|
||||
# This package is not intended for consumption on pub.dev. DO NOT publish.
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=2.8.1 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
path: any
|
||||
yaml: any
|
||||
|
||||
dev_dependencies:
|
||||
pedantic: ^1.9.0
|
Loading…
Reference in a new issue