Only call Package.listFiles once per publish. (#3346)

diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart
index 916f12d..a20fbdf 100644
--- a/lib/src/command/lish.dart
+++ b/lib/src/command/lish.dart
@@ -229,8 +229,8 @@
         createTarGz(files, baseDir: entrypoint.root.dir).toBytes();
 
     // Validate the package.
-    var isValid =
-        await _validate(packageBytesFuture.then((bytes) => bytes.length));
+    var isValid = await _validate(
+        packageBytesFuture.then((bytes) => bytes.length), files);
     if (!isValid) {
       overrideExitCode(exit_codes.DATA);
       return;
@@ -251,7 +251,7 @@
 
   /// Validates the package. Completes to false if the upload should not
   /// proceed.
-  Future<bool> _validate(Future<int> packageSize) async {
+  Future<bool> _validate(Future<int> packageSize, List<String> files) async {
     final hints = <String>[];
     final warnings = <String>[];
     final errors = <String>[];
@@ -260,6 +260,7 @@
       entrypoint,
       packageSize,
       host,
+      files,
       hints: hints,
       warnings: warnings,
       errors: errors,
diff --git a/lib/src/package.dart b/lib/src/package.dart
index e3b7981..4d90311 100644
--- a/lib/src/package.dart
+++ b/lib/src/package.dart
@@ -4,7 +4,6 @@
 
 import 'dart:io';
 
-import 'package:collection/collection.dart' show IterableExtension;
 import 'package:path/path.dart' as p;
 import 'package:pub_semver/pub_semver.dart';
 
@@ -18,9 +17,6 @@
 import 'system_cache.dart';
 import 'utils.dart';
 
-final _readmeRegexp = RegExp(r'^README($|\.)', caseSensitive: false);
-final _changelogRegexp = RegExp(r'^CHANGELOG($|\.)', caseSensitive: false);
-
 /// A named, versioned, unit of code and resource reuse.
 class Package {
   /// Compares [a] and [b] orders them by name then version number.
@@ -98,34 +94,6 @@
   List<String> get executableNames =>
       executablePaths.map(p.basenameWithoutExtension).toList();
 
-  /// Returns the path to the README file at the root of the entrypoint, or null
-  /// if no README file is found.
-  ///
-  /// If multiple READMEs are found, this uses the same conventions as
-  /// pub.dartlang.org for choosing the primary one: the README with the fewest
-  /// extensions that is lexically ordered first is chosen.
-  String? get readmePath {
-    var readmes = listFiles(recursive: false)
-        .map(p.basename)
-        .where((entry) => entry.contains(_readmeRegexp));
-    if (readmes.isEmpty) return null;
-
-    return p.join(dir, readmes.reduce((readme1, readme2) {
-      var extensions1 = '.'.allMatches(readme1).length;
-      var extensions2 = '.'.allMatches(readme2).length;
-      var comparison = extensions1.compareTo(extensions2);
-      if (comparison == 0) comparison = readme1.compareTo(readme2);
-      return (comparison <= 0) ? readme1 : readme2;
-    }));
-  }
-
-  /// Returns the path to the CHANGELOG file at the root of the entrypoint, or
-  /// null if no CHANGELOG file is found.
-  String? get changelogPath {
-    return listFiles(recursive: false).firstWhereOrNull(
-        (entry) => p.basename(entry).contains(_changelogRegexp));
-  }
-
   /// Returns whether or not this package is in a Git repo.
   late final bool inGitRepo = computeInGitRepoCache();
 
diff --git a/lib/src/validator.dart b/lib/src/validator.dart
index 1b07560..ef04bc4 100644
--- a/lib/src/validator.dart
+++ b/lib/src/validator.dart
@@ -5,6 +5,7 @@
 import 'dart:async';
 
 import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
 import 'package:pub_semver/pub_semver.dart';
 
 import 'entrypoint.dart';
@@ -42,9 +43,6 @@
 /// package not to be uploaded; warnings will require the user to confirm the
 /// upload.
 abstract class Validator {
-  /// The entrypoint that's being validated.
-  final Entrypoint entrypoint;
-
   /// The accumulated errors for this validator.
   ///
   /// Filled by calling [validate].
@@ -60,11 +58,15 @@
   /// Filled by calling [validate].
   final hints = <String>[];
 
-  Validator(this.entrypoint);
+  late ValidationContext context;
+  Entrypoint get entrypoint => context.entrypoint;
+  int get packageSize => context.packageSize;
+  Uri get serverUrl => context.serverUrl;
+  List<String> get files => context.files;
 
   /// Validates the entrypoint, adding any errors and warnings to [errors] and
   /// [warnings], respectively.
-  Future validate();
+  Future<void> validate();
 
   /// Adds an error if the package's SDK constraint doesn't exclude Dart SDK
   /// versions older than [firstSdkVersion].
@@ -114,45 +116,55 @@
 
   /// Run all validators on the [entrypoint] package and print their results.
   ///
+  /// [files] should be the result of `entrypoint.root.listFiles()`.
+  ///
   /// When the future completes [hints] [warnings] amd [errors] will have been
   /// appended with the reported hints warnings and errors respectively.
   ///
   /// [packageSize], if passed, should complete to the size of the tarred
   /// package, in bytes. This is used to validate that it's not too big to
   /// upload to the server.
-  static Future<void> runAll(
-      Entrypoint entrypoint, Future<int> packageSize, Uri? serverUrl,
+  static Future<void> runAll(Entrypoint entrypoint, Future<int> packageSize,
+      Uri serverUrl, List<String> files,
       {required List<String> hints,
       required List<String> warnings,
-      required List<String> errors}) {
+      required List<String> errors}) async {
     var validators = [
-      GitignoreValidator(entrypoint),
-      PubspecValidator(entrypoint),
-      LicenseValidator(entrypoint),
-      NameValidator(entrypoint),
-      PubspecFieldValidator(entrypoint),
-      DependencyValidator(entrypoint),
-      DependencyOverrideValidator(entrypoint),
-      DeprecatedFieldsValidator(entrypoint),
-      DirectoryValidator(entrypoint),
-      ExecutableValidator(entrypoint),
-      CompiledDartdocValidator(entrypoint),
-      ReadmeValidator(entrypoint),
-      ChangelogValidator(entrypoint),
-      SdkConstraintValidator(entrypoint),
-      StrictDependenciesValidator(entrypoint),
-      FlutterConstraintValidator(entrypoint),
-      FlutterPluginFormatValidator(entrypoint),
-      LanguageVersionValidator(entrypoint),
-      RelativeVersionNumberingValidator(entrypoint, serverUrl),
-      NullSafetyMixedModeValidator(entrypoint),
-      PubspecTypoValidator(entrypoint),
-      LeakDetectionValidator(entrypoint),
+      GitignoreValidator(),
+      PubspecValidator(),
+      LicenseValidator(),
+      NameValidator(),
+      PubspecFieldValidator(),
+      DependencyValidator(),
+      DependencyOverrideValidator(),
+      DeprecatedFieldsValidator(),
+      DirectoryValidator(),
+      ExecutableValidator(),
+      CompiledDartdocValidator(),
+      ReadmeValidator(),
+      ChangelogValidator(),
+      SdkConstraintValidator(),
+      StrictDependenciesValidator(),
+      FlutterConstraintValidator(),
+      FlutterPluginFormatValidator(),
+      LanguageVersionValidator(),
+      RelativeVersionNumberingValidator(),
+      NullSafetyMixedModeValidator(),
+      PubspecTypoValidator(),
+      LeakDetectionValidator(),
+      SizeValidator(),
     ];
-    validators.add(SizeValidator(entrypoint, packageSize));
 
-    return Future.wait(validators.map((validator) => validator.validate()))
-        .then((_) {
+    final context = ValidationContext(
+      entrypoint,
+      await packageSize,
+      serverUrl,
+      files,
+    );
+    return await Future.wait(validators.map((validator) async {
+      validator.context = context;
+      await validator.validate();
+    })).then((_) {
       hints.addAll([for (final validator in validators) ...validator.hints]);
       warnings
           .addAll([for (final validator in validators) ...validator.warnings]);
@@ -190,4 +202,28 @@
       }
     });
   }
+
+  /// Returns the [files] that are inside [dir] (relative to the package
+  /// entrypoint).
+  // TODO(sigurdm): Consider moving this to a more central location.
+  List<String> filesBeneath(String dir, {required bool recursive}) {
+    final base = p.canonicalize(p.join(entrypoint.root.dir, dir));
+    return files
+        .where(
+          recursive
+              ? (file) => p.canonicalize(file).startsWith(base)
+              : (file) => p.canonicalize(p.dirname(file)) == base,
+        )
+        .toList();
+  }
+}
+
+class ValidationContext {
+  final Entrypoint entrypoint;
+  final int packageSize;
+  final Uri serverUrl;
+  final List<String> files;
+
+  ValidationContext(
+      this.entrypoint, this.packageSize, this.serverUrl, this.files);
 }
diff --git a/lib/src/validator/changelog.dart b/lib/src/validator/changelog.dart
index 614eba2..bb897a7 100644
--- a/lib/src/validator/changelog.dart
+++ b/lib/src/validator/changelog.dart
@@ -5,53 +5,52 @@
 import 'dart:async';
 import 'dart:convert';
 
+import 'package:collection/collection.dart';
 import 'package:path/path.dart' as p;
 
-import '../entrypoint.dart';
 import '../io.dart';
 import '../validator.dart';
 
+final _changelogRegexp = RegExp(r'^CHANGELOG($|\.)', caseSensitive: false);
+
 /// A validator that validates a package's changelog file.
 class ChangelogValidator extends Validator {
-  ChangelogValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
-  Future validate() {
-    return Future.sync(() {
-      final changelog = entrypoint.root.changelogPath;
+  Future<void> validate() async {
+    final changelog = filesBeneath('.', recursive: false).firstWhereOrNull(
+        (entry) => p.basename(entry).contains(_changelogRegexp));
 
-      if (changelog == null) {
-        warnings.add('Please add a `CHANGELOG.md` to your package. '
-            'See https://dart.dev/tools/pub/publishing#important-files.');
-        return;
-      }
+    if (changelog == null) {
+      warnings.add('Please add a `CHANGELOG.md` to your package. '
+          'See https://dart.dev/tools/pub/publishing#important-files.');
+      return;
+    }
 
-      if (p.basename(changelog) != 'CHANGELOG.md') {
-        warnings.add('Please consider renaming $changelog to `CHANGELOG.md`. '
-            'See https://dart.dev/tools/pub/publishing#important-files.');
-      }
+    if (p.basename(changelog) != 'CHANGELOG.md') {
+      warnings.add('Please consider renaming $changelog to `CHANGELOG.md`. '
+          'See https://dart.dev/tools/pub/publishing#important-files.');
+    }
 
-      var bytes = readBinaryFile(changelog);
-      String contents;
+    var bytes = readBinaryFile(changelog);
+    String contents;
 
-      try {
-        // utf8.decode doesn't allow invalid UTF-8.
-        contents = utf8.decode(bytes);
-      } on FormatException catch (_) {
-        warnings.add('$changelog contains invalid UTF-8.\n'
-            'This will cause it to be displayed incorrectly on '
-            'the Pub site (https://pub.dev).');
-        // Failed to decode contents, so there's nothing else to check.
-        return;
-      }
+    try {
+      // utf8.decode doesn't allow invalid UTF-8.
+      contents = utf8.decode(bytes);
+    } on FormatException catch (_) {
+      warnings.add('$changelog contains invalid UTF-8.\n'
+          'This will cause it to be displayed incorrectly on '
+          'the Pub site (https://pub.dev).');
+      // Failed to decode contents, so there's nothing else to check.
+      return;
+    }
 
-      final version = entrypoint.root.pubspec.version.toString();
+    final version = entrypoint.root.pubspec.version.toString();
 
-      if (!contents.contains(version)) {
-        warnings.add("$changelog doesn't mention current version ($version).\n"
-            'Consider updating it with notes on this version prior to '
-            'publication.');
-      }
-    });
+    if (!contents.contains(version)) {
+      warnings.add("$changelog doesn't mention current version ($version).\n"
+          'Consider updating it with notes on this version prior to '
+          'publication.');
+    }
   }
 }
diff --git a/lib/src/validator/compiled_dartdoc.dart b/lib/src/validator/compiled_dartdoc.dart
index 3f3376a..ae25f83 100644
--- a/lib/src/validator/compiled_dartdoc.dart
+++ b/lib/src/validator/compiled_dartdoc.dart
@@ -6,19 +6,16 @@
 
 import 'package:path/path.dart' as path;
 
-import '../entrypoint.dart';
 import '../io.dart';
 import '../validator.dart';
 
 /// Validates that a package doesn't contain compiled Dartdoc
 /// output.
 class CompiledDartdocValidator extends Validator {
-  CompiledDartdocValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
   Future validate() {
     return Future.sync(() {
-      for (var entry in entrypoint.root.listFiles()) {
+      for (var entry in files) {
         if (path.basename(entry) != 'nav.json') continue;
         var dir = path.dirname(entry);
 
diff --git a/lib/src/validator/dependency.dart b/lib/src/validator/dependency.dart
index fd8a058..40e9c2a 100644
--- a/lib/src/validator/dependency.dart
+++ b/lib/src/validator/dependency.dart
@@ -6,7 +6,6 @@
 
 import 'package:pub_semver/pub_semver.dart';
 
-import '../entrypoint.dart';
 import '../exceptions.dart';
 import '../log.dart' as log;
 import '../package_name.dart';
@@ -25,13 +24,202 @@
 
 /// A validator that validates a package's dependencies.
 class DependencyValidator extends Validator {
-  /// Whether any dependency has a caret constraint.
-  var _hasCaretDep = false;
-
-  DependencyValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
   Future validate() async {
+    /// Whether any dependency has a caret constraint.
+    var _hasCaretDep = false;
+
+    /// Emit an error for dependencies from unknown SDKs or without appropriate
+    /// constraints on the Dart SDK.
+    void _warnAboutSdkSource(PackageRange dep) {
+      var identifier = (dep.description as SdkDescription).sdk;
+      var sdk = sdks[identifier];
+      if (sdk == null) {
+        errors.add('Unknown SDK "$identifier" for dependency "${dep.name}".');
+        return;
+      }
+
+      validateSdkConstraint(sdk.firstPubVersion,
+          "Older versions of pub don't support the ${sdk.name} SDK.");
+    }
+
+    /// Warn that dependencies should use the hosted source.
+    Future _warnAboutSource(PackageRange dep) async {
+      List<Version> versions;
+      try {
+        var ids = await entrypoint.cache
+            .getVersions(entrypoint.cache.hosted.refFor(dep.name));
+        versions = ids.map((id) => id.version).toList();
+      } on ApplicationException catch (_) {
+        versions = [];
+      }
+
+      String constraint;
+      if (versions.isNotEmpty) {
+        constraint = '^${Version.primary(versions)}';
+      } else {
+        constraint = dep.constraint.toString();
+        if (!dep.constraint.isAny && dep.constraint is! Version) {
+          constraint = '"$constraint"';
+        }
+      }
+
+      // Path sources are errors. Other sources are just warnings.
+      var messages = dep.source is PathSource ? errors : warnings;
+
+      messages.add('Don\'t depend on "${dep.name}" from the ${dep.source} '
+          'source. Use the hosted source instead. For example:\n'
+          '\n'
+          'dependencies:\n'
+          '  ${dep.name}: $constraint\n'
+          '\n'
+          'Using the hosted source ensures that everyone can download your '
+          'package\'s dependencies along with your package.');
+    }
+
+    /// Warn about improper dependencies on Flutter.
+    void _warnAboutFlutterSdk(PackageRange dep) {
+      if (dep.source is SdkSource) {
+        _warnAboutSdkSource(dep);
+        return;
+      }
+
+      errors.add('Don\'t depend on "${dep.name}" from the ${dep.source} '
+          'source. Use the SDK source instead. For example:\n'
+          '\n'
+          'dependencies:\n'
+          '  ${dep.name}:\n'
+          '    sdk: ${dep.constraint}\n'
+          '\n'
+          'The Flutter SDK is downloaded and managed outside of pub.');
+    }
+
+    /// Warn that dependencies should have version constraints.
+    void _warnAboutNoConstraint(PackageRange dep) {
+      var message = 'Your dependency on "${dep.name}" should have a version '
+          'constraint.';
+      var locked = entrypoint.lockFile.packages[dep.name];
+      if (locked != null) {
+        message = '$message For example:\n'
+            '\n'
+            'dependencies:\n'
+            '  ${dep.name}: ^${locked.version}\n';
+      }
+      warnings.add('$message\n'
+          'Without a constraint, you\'re promising to support ${log.bold("all")} '
+          'future versions of "${dep.name}".');
+    }
+
+    /// Warn that dependencies should allow more than a single version.
+    void _warnAboutSingleVersionConstraint(PackageRange dep) {
+      warnings.add(
+          'Your dependency on "${dep.name}" should allow more than one version. '
+          'For example:\n'
+          '\n'
+          'dependencies:\n'
+          '  ${dep.name}: ^${dep.constraint}\n'
+          '\n'
+          'Constraints that are too tight will make it difficult for people to '
+          'use your package\n'
+          'along with other packages that also depend on "${dep.name}".');
+    }
+
+    /// Warn that dependencies should have lower bounds on their constraints.
+    void _warnAboutNoConstraintLowerBound(PackageRange dep) {
+      var message =
+          'Your dependency on "${dep.name}" should have a lower bound.';
+      var locked = entrypoint.lockFile.packages[dep.name];
+      if (locked != null) {
+        String constraint;
+        if (locked.version == (dep.constraint as VersionRange).max) {
+          constraint = '^${locked.version}';
+        } else {
+          constraint = '">=${locked.version} ${dep.constraint}"';
+        }
+
+        message = '$message For example:\n'
+            '\n'
+            'dependencies:\n'
+            '  ${dep.name}: $constraint\n';
+      }
+      warnings.add('$message\n'
+          'Without a constraint, you\'re promising to support ${log.bold("all")} '
+          'previous versions of "${dep.name}".');
+    }
+
+    /// Warn that dependencies should have upper bounds on their constraints.
+    void _warnAboutNoConstraintUpperBound(PackageRange dep) {
+      String constraint;
+      if ((dep.constraint as VersionRange).includeMin) {
+        constraint = '^${(dep.constraint as VersionRange).min}';
+      } else {
+        constraint = '"${dep.constraint} '
+            '<${(dep.constraint as VersionRange).min!.nextBreaking}"';
+      }
+      // TODO: Handle the case where `dep.constraint.min` is null.
+
+      warnings.add(
+          'Your dependency on "${dep.name}" should have an upper bound. For '
+          'example:\n'
+          '\n'
+          'dependencies:\n'
+          '  ${dep.name}: $constraint\n'
+          '\n'
+          'Without an upper bound, you\'re promising to support '
+          '${log.bold("all")} future versions of ${dep.name}.');
+    }
+
+    void _warnAboutPrerelease(String dependencyName, VersionRange constraint) {
+      final packageVersion = entrypoint.root.version;
+      if (constraint.min != null &&
+          constraint.min!.isPreRelease &&
+          !packageVersion.isPreRelease) {
+        warnings.add('Packages dependent on a pre-release of another package '
+            'should themselves be published as a pre-release version. '
+            'If this package needs $dependencyName version ${constraint.min}, '
+            'consider publishing the package as a pre-release instead.\n'
+            'See https://dart.dev/tools/pub/publishing#publishing-prereleases '
+            'For more information on pre-releases.');
+      }
+    }
+
+    /// Validates all dependencies in [dependencies].
+    Future _validateDependencies(Iterable<PackageRange> dependencies) async {
+      for (var dependency in dependencies) {
+        var constraint = dependency.constraint;
+        if (dependency.name == 'flutter') {
+          _warnAboutFlutterSdk(dependency);
+        } else if (dependency.source is SdkSource) {
+          _warnAboutSdkSource(dependency);
+        } else if (dependency.source is! HostedSource) {
+          await _warnAboutSource(dependency);
+
+          final description = dependency.description;
+          if (description is GitDescription && description.path != '.') {
+            validateSdkConstraint(_firstGitPathVersion,
+                "Older versions of pub don't support Git path dependencies.");
+          }
+        } else {
+          if (constraint.isAny) {
+            _warnAboutNoConstraint(dependency);
+          } else if (constraint is VersionRange) {
+            if (constraint is Version) {
+              _warnAboutSingleVersionConstraint(dependency);
+            } else {
+              _warnAboutPrerelease(dependency.name, constraint);
+              if (constraint.min == null) {
+                _warnAboutNoConstraintLowerBound(dependency);
+              } else if (constraint.max == null) {
+                _warnAboutNoConstraintUpperBound(dependency);
+              }
+            }
+            _hasCaretDep =
+                _hasCaretDep || constraint.toString().startsWith('^');
+          }
+        }
+      }
+    }
+
     await _validateDependencies(entrypoint.root.pubspec.dependencies.values);
 
     if (_hasCaretDep) {
@@ -39,197 +227,4 @@
           "Older versions of pub don't support ^ version constraints.");
     }
   }
-
-  /// Validates all dependencies in [dependencies].
-  Future _validateDependencies(Iterable<PackageRange> dependencies) async {
-    for (var dependency in dependencies) {
-      var constraint = dependency.constraint;
-      if (dependency.name == 'flutter') {
-        _warnAboutFlutterSdk(dependency);
-      } else if (dependency.source is SdkSource) {
-        _warnAboutSdkSource(dependency);
-      } else if (dependency.source is HostedSource) {
-        if (constraint.isAny) {
-          _warnAboutNoConstraint(dependency);
-        } else if (constraint is VersionRange) {
-          if (constraint is Version) {
-            _warnAboutSingleVersionConstraint(dependency);
-          } else {
-            _warnAboutPrerelease(dependency.name, constraint);
-            if (constraint.min == null) {
-              _warnAboutNoConstraintLowerBound(dependency);
-            } else if (constraint.max == null) {
-              _warnAboutNoConstraintUpperBound(dependency);
-            }
-          }
-          _hasCaretDep = _hasCaretDep || constraint.toString().startsWith('^');
-        }
-      } else {
-        await _warnAboutSource(dependency);
-        final description = dependency.description;
-
-        if (description is GitDescription && description.path != '.') {
-          validateSdkConstraint(_firstGitPathVersion,
-              "Older versions of pub don't support Git path dependencies.");
-        }
-      }
-    }
-  }
-
-  /// Warn about improper dependencies on Flutter.
-  void _warnAboutFlutterSdk(PackageRange dep) {
-    if (dep.source is SdkSource) {
-      _warnAboutSdkSource(dep);
-      return;
-    }
-
-    errors.add('Don\'t depend on "${dep.name}" from the ${dep.source} '
-        'source. Use the SDK source instead. For example:\n'
-        '\n'
-        'dependencies:\n'
-        '  ${dep.name}:\n'
-        '    sdk: ${dep.constraint}\n'
-        '\n'
-        'The Flutter SDK is downloaded and managed outside of pub.');
-  }
-
-  /// Emit an error for dependencies from unknown SDKs or without appropriate
-  /// constraints on the Dart SDK.
-  void _warnAboutSdkSource(PackageRange dep) {
-    final description = dep.description;
-    if (description is! SdkDescription) {
-      throw ArgumentError('Wrong source');
-    }
-    var identifier = description.sdk;
-    var sdk = sdks[identifier];
-    if (sdk == null) {
-      errors.add('Unknown SDK "$identifier" for dependency "${dep.name}".');
-      return;
-    }
-
-    validateSdkConstraint(sdk.firstPubVersion,
-        "Older versions of pub don't support the ${sdk.name} SDK.");
-  }
-
-  /// Warn that dependencies should use the hosted source.
-  Future _warnAboutSource(PackageRange dep) async {
-    List<Version> versions;
-    try {
-      var ids = await entrypoint.cache
-          .getVersions(entrypoint.cache.hosted.refFor(dep.name));
-      versions = ids.map((id) => id.version).toList();
-    } on ApplicationException catch (_) {
-      versions = [];
-    }
-
-    late String constraint;
-    if (versions.isNotEmpty) {
-      constraint = '^${Version.primary(versions)}';
-    } else {
-      constraint = dep.constraint.toString();
-      if (!dep.constraint.isAny && dep.constraint is! Version) {
-        constraint = '"$constraint"';
-      }
-    }
-
-    // Path sources are errors. Other sources are just warnings.
-    var messages = dep.source is PathSource ? errors : warnings;
-
-    messages.add('Don\'t depend on "${dep.name}" from the ${dep.source} '
-        'source. Use the hosted source instead. For example:\n'
-        '\n'
-        'dependencies:\n'
-        '  ${dep.name}: $constraint\n'
-        '\n'
-        'Using the hosted source ensures that everyone can download your '
-        'package\'s dependencies along with your package.');
-  }
-
-  /// Warn that dependencies should have version constraints.
-  void _warnAboutNoConstraint(PackageRange dep) {
-    var message = 'Your dependency on "${dep.name}" should have a version '
-        'constraint.';
-    var locked = entrypoint.lockFile.packages[dep.name];
-    if (locked != null) {
-      message = '$message For example:\n'
-          '\n'
-          'dependencies:\n'
-          '  ${dep.name}: ^${locked.version}\n';
-    }
-    warnings.add('$message\n'
-        'Without a constraint, you\'re promising to support ${log.bold("all")} '
-        'future versions of "${dep.name}".');
-  }
-
-  /// Warn that dependencies should allow more than a single version.
-  void _warnAboutSingleVersionConstraint(PackageRange dep) {
-    warnings.add(
-        'Your dependency on "${dep.name}" should allow more than one version. '
-        'For example:\n'
-        '\n'
-        'dependencies:\n'
-        '  ${dep.name}: ^${dep.constraint}\n'
-        '\n'
-        'Constraints that are too tight will make it difficult for people to '
-        'use your package\n'
-        'along with other packages that also depend on "${dep.name}".');
-  }
-
-  /// Warn that dependencies should have lower bounds on their constraints.
-  void _warnAboutNoConstraintLowerBound(PackageRange dep) {
-    var message = 'Your dependency on "${dep.name}" should have a lower bound.';
-    var locked = entrypoint.lockFile.packages[dep.name];
-    if (locked != null) {
-      String constraint;
-      if (locked.version == (dep.constraint as VersionRange).max) {
-        constraint = '^${locked.version}';
-      } else {
-        constraint = '">=${locked.version} ${dep.constraint}"';
-      }
-
-      message = '$message For example:\n'
-          '\n'
-          'dependencies:\n'
-          '  ${dep.name}: $constraint\n';
-    }
-    warnings.add('$message\n'
-        'Without a constraint, you\'re promising to support ${log.bold("all")} '
-        'previous versions of "${dep.name}".');
-  }
-
-  /// Warn that dependencies should have upper bounds on their constraints.
-  void _warnAboutNoConstraintUpperBound(PackageRange dep) {
-    String constraint;
-    if ((dep.constraint as VersionRange).includeMin) {
-      constraint = '^${(dep.constraint as VersionRange).min}';
-    } else {
-      constraint = '"${dep.constraint} '
-          '<${(dep.constraint as VersionRange).min!.nextBreaking}"';
-    }
-    // TODO: Handle the case where `dep.constraint.min` is null.
-
-    warnings
-        .add('Your dependency on "${dep.name}" should have an upper bound. For '
-            'example:\n'
-            '\n'
-            'dependencies:\n'
-            '  ${dep.name}: $constraint\n'
-            '\n'
-            'Without an upper bound, you\'re promising to support '
-            '${log.bold("all")} future versions of ${dep.name}.');
-  }
-
-  void _warnAboutPrerelease(String dependencyName, VersionRange constraint) {
-    final packageVersion = entrypoint.root.version;
-    if (constraint.min != null &&
-        constraint.min!.isPreRelease &&
-        !packageVersion.isPreRelease) {
-      warnings.add('Packages dependent on a pre-release of another package '
-          'should themselves be published as a pre-release version. '
-          'If this package needs $dependencyName version ${constraint.min}, '
-          'consider publishing the package as a pre-release instead.\n'
-          'See https://dart.dev/tools/pub/publishing#publishing-prereleases '
-          'For more information on pre-releases.');
-    }
-  }
 }
diff --git a/lib/src/validator/dependency_override.dart b/lib/src/validator/dependency_override.dart
index 24e4e84..b6a49a0 100644
--- a/lib/src/validator/dependency_override.dart
+++ b/lib/src/validator/dependency_override.dart
@@ -6,14 +6,11 @@
 
 import 'package:collection/collection.dart';
 
-import '../entrypoint.dart';
 import '../validator.dart';
 
 /// A validator that validates a package's dependencies overrides (or the
 /// absence thereof).
 class DependencyOverrideValidator extends Validator {
-  DependencyOverrideValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
   Future validate() {
     var overridden = MapKeySet(entrypoint.root.dependencyOverrides);
diff --git a/lib/src/validator/deprecated_fields.dart b/lib/src/validator/deprecated_fields.dart
index e1aee4f..942c8f5 100644
--- a/lib/src/validator/deprecated_fields.dart
+++ b/lib/src/validator/deprecated_fields.dart
@@ -4,14 +4,11 @@
 
 import 'dart:async';
 
-import '../entrypoint.dart';
 import '../validator.dart';
 
 /// A validator that validates that a pubspec is not including deprecated fields
 /// which are no longer read.
 class DeprecatedFieldsValidator extends Validator {
-  DeprecatedFieldsValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
   Future validate() async {
     if (entrypoint.root.pubspec.fields.containsKey('transformers')) {
diff --git a/lib/src/validator/directory.dart b/lib/src/validator/directory.dart
index c487dc9..ae8ac57 100644
--- a/lib/src/validator/directory.dart
+++ b/lib/src/validator/directory.dart
@@ -6,14 +6,11 @@
 
 import 'package:path/path.dart' as path;
 
-import '../entrypoint.dart';
 import '../io.dart';
 import '../validator.dart';
 
 /// A validator that validates a package's top-level directories.
 class DirectoryValidator extends Validator {
-  DirectoryValidator(Entrypoint entrypoint) : super(entrypoint);
-
   static final _pluralNames = [
     'benchmarks',
     'docs',
@@ -27,7 +24,7 @@
   @override
   Future<void> validate() async {
     final visited = <String>{};
-    for (final file in entrypoint.root.listFiles()) {
+    for (final file in files) {
       // Find the topmost directory name of [file].
       final dir = path.join(entrypoint.root.dir,
           path.split(path.relative(file, from: entrypoint.root.dir)).first);
diff --git a/lib/src/validator/executable.dart b/lib/src/validator/executable.dart
index 2163e8c..b7679eb 100644
--- a/lib/src/validator/executable.dart
+++ b/lib/src/validator/executable.dart
@@ -6,20 +6,15 @@
 
 import 'package:path/path.dart' as p;
 
-import '../entrypoint.dart';
 import '../validator.dart';
 
 /// Validates that a package's pubspec doesn't contain executables that
 /// reference non-existent scripts.
 class ExecutableValidator extends Validator {
-  ExecutableValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
   Future validate() async {
-    var binFiles = entrypoint.root
-        .listFiles(beneath: 'bin', recursive: false)
-        .map(entrypoint.root.relative)
-        .toList();
+    final binFiles =
+        filesBeneath('bin', recursive: false).map(entrypoint.root.relative);
 
     entrypoint.root.pubspec.executables.forEach((executable, script) {
       var scriptPath = p.join('bin', '$script.dart');
diff --git a/lib/src/validator/flutter_constraint.dart b/lib/src/validator/flutter_constraint.dart
index d689a22..1b1e224 100644
--- a/lib/src/validator/flutter_constraint.dart
+++ b/lib/src/validator/flutter_constraint.dart
@@ -6,12 +6,10 @@
 
 import 'package:pub_semver/pub_semver.dart';
 
-import '../entrypoint.dart';
 import '../validator.dart';
 
 /// Validates that a package's flutter constraint doesn't contain an upper bound
 class FlutterConstraintValidator extends Validator {
-  FlutterConstraintValidator(Entrypoint entrypoint) : super(entrypoint);
   static const explanationUrl =
       'https://dart.dev/go/flutter-upper-bound-deprecation';
 
diff --git a/lib/src/validator/flutter_plugin_format.dart b/lib/src/validator/flutter_plugin_format.dart
index 6f00f06..a56f7df 100644
--- a/lib/src/validator/flutter_plugin_format.dart
+++ b/lib/src/validator/flutter_plugin_format.dart
@@ -6,7 +6,6 @@
 
 import 'package:pub_semver/pub_semver.dart';
 
-import '../entrypoint.dart';
 import '../validator.dart';
 
 const _pluginDocsUrl =
@@ -19,8 +18,6 @@
 /// See:
 /// https://flutter.dev/docs/development/packages-and-plugins/developing-packages
 class FlutterPluginFormatValidator extends Validator {
-  FlutterPluginFormatValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
   Future validate() async {
     final pubspec = entrypoint.root.pubspec;
diff --git a/lib/src/validator/gitignore.dart b/lib/src/validator/gitignore.dart
index 532178b..809c537 100644
--- a/lib/src/validator/gitignore.dart
+++ b/lib/src/validator/gitignore.dart
@@ -7,7 +7,6 @@
 
 import 'package:path/path.dart' as p;
 
-import '../entrypoint.dart';
 import '../git.dart' as git;
 import '../ignore.dart';
 import '../io.dart';
@@ -19,8 +18,6 @@
 /// .gitignore. These would be considered part of the package by previous
 /// versions of pub.
 class GitignoreValidator extends Validator {
-  GitignoreValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
   Future<void> validate() async {
     if (entrypoint.root.inGitRepo) {
diff --git a/lib/src/validator/language_version.dart b/lib/src/validator/language_version.dart
index e52a92a..2041bcb 100644
--- a/lib/src/validator/language_version.dart
+++ b/lib/src/validator/language_version.dart
@@ -9,7 +9,6 @@
 import 'package:stack_trace/stack_trace.dart';
 
 import '../dart.dart';
-import '../entrypoint.dart';
 import '../language_version.dart';
 import '../log.dart' as log;
 import '../utils.dart';
@@ -18,22 +17,18 @@
 /// Validates that libraries do not opt into newer language versions than what
 /// they declare in their pubspec.
 class LanguageVersionValidator extends Validator {
-  final AnalysisContextManager analysisContextManager =
-      AnalysisContextManager();
-
-  LanguageVersionValidator(Entrypoint entrypoint) : super(entrypoint) {
-    var packagePath = p.normalize(p.absolute(entrypoint.root.dir));
-    analysisContextManager.createContextsForDirectory(packagePath);
-  }
-
   @override
   Future validate() async {
+    var packagePath = p.normalize(p.absolute(entrypoint.root.dir));
+    final analysisContextManager = AnalysisContextManager()
+      ..createContextsForDirectory(packagePath);
+
     final declaredLanguageVersion = entrypoint.root.pubspec.languageVersion;
 
-    for (final path in ['lib', 'bin']
-        .map((path) => entrypoint.root.listFiles(beneath: path))
-        .expand((files) => files)
-        .where((String file) => p.extension(file) == '.dart')) {
+    for (final path in ['lib', 'bin'].expand((path) {
+      return filesBeneath(path, recursive: true)
+          .where((file) => p.extension(file) == '.dart');
+    })) {
       CompilationUnit unit;
       try {
         unit = analysisContextManager.parse(path);
diff --git a/lib/src/validator/leak_detection.dart b/lib/src/validator/leak_detection.dart
index 7a398d4..03100f8 100644
--- a/lib/src/validator/leak_detection.dart
+++ b/lib/src/validator/leak_detection.dart
@@ -12,7 +12,6 @@
 import 'package:pool/pool.dart';
 import 'package:source_span/source_span.dart';
 
-import '../entrypoint.dart';
 import '../ignore.dart';
 import '../validator.dart';
 
@@ -26,8 +25,6 @@
 /// accidentally leaked.
 @sealed
 class LeakDetectionValidator extends Validator {
-  LeakDetectionValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
   Future<void> validate() async {
     // Load `false_secrets` from `pubspec.yaml`.
@@ -37,7 +34,7 @@
     );
 
     final pool = Pool(20); // don't read more than 20 files concurrently!
-    final leaks = await Future.wait(entrypoint.root.listFiles().map((f) async {
+    final leaks = await Future.wait(files.map((f) async {
       final relPath = entrypoint.root.relative(f);
 
       // Skip files matching patterns in `false_secrets`
diff --git a/lib/src/validator/license.dart b/lib/src/validator/license.dart
index 1a2c68a..993a48f 100644
--- a/lib/src/validator/license.dart
+++ b/lib/src/validator/license.dart
@@ -4,26 +4,23 @@
 
 import 'dart:async';
 
-import 'package:path/path.dart' as path;
+import 'package:path/path.dart' as p;
 
-import '../entrypoint.dart';
 import '../validator.dart';
 
+final licenseLike =
+    RegExp(r'^(([a-zA-Z0-9]+[-_])?(LICENSE|COPYING)|UNLICENSE)(\..*)?$');
+
 /// A validator that checks that a LICENSE-like file exists.
 class LicenseValidator extends Validator {
-  LicenseValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
   Future validate() {
     return Future.sync(() {
-      final licenseLike =
-          RegExp(r'^(([a-zA-Z0-9]+[-_])?(LICENSE|COPYING)|UNLICENSE)(\..*)?$');
-      final candidates = entrypoint.root
-          .listFiles(recursive: false)
-          .map(path.basename)
-          .where(licenseLike.hasMatch);
+      final candidates = filesBeneath('.', recursive: false)
+          .where((file) => licenseLike.hasMatch(p.basename(file)));
       if (candidates.isNotEmpty) {
-        if (!candidates.contains('LICENSE')) {
+        if (!candidates
+            .any((candidate) => p.basename(candidate) == 'LICENSE')) {
           final firstCandidate = candidates.first;
           warnings.add('Please consider renaming $firstCandidate to `LICENSE`. '
               'See https://dart.dev/tools/pub/publishing#important-files.');
diff --git a/lib/src/validator/name.dart b/lib/src/validator/name.dart
index 07d945c..bc02031 100644
--- a/lib/src/validator/name.dart
+++ b/lib/src/validator/name.dart
@@ -6,21 +6,18 @@
 
 import 'package:path/path.dart' as path;
 
-import '../entrypoint.dart';
 import '../utils.dart';
 import '../validator.dart';
 
 /// A validator that the name of a package is legal and matches the library name
 /// in the case of a single library.
 class NameValidator extends Validator {
-  NameValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
   Future validate() {
     return Future.sync(() {
       _checkName(entrypoint.root.name);
 
-      var libraries = _libraries;
+      var libraries = _libraries(files);
 
       if (libraries.length == 1) {
         var libName = path.basenameWithoutExtension(libraries[0]);
@@ -34,10 +31,9 @@
 
   /// Returns a list of all libraries in the current package as paths relative
   /// to the package's root directory.
-  List<String> get _libraries {
+  List<String> _libraries(List<String> files) {
     var libDir = entrypoint.root.path('lib');
-    return entrypoint.root
-        .listFiles(beneath: 'lib')
+    return filesBeneath('lib', recursive: true)
         .map((file) => path.relative(file, from: path.dirname(libDir)))
         .where((file) =>
             !path.split(file).contains('src') &&
diff --git a/lib/src/validator/null_safety_mixed_mode.dart b/lib/src/validator/null_safety_mixed_mode.dart
index 10be199..36f9796 100644
--- a/lib/src/validator/null_safety_mixed_mode.dart
+++ b/lib/src/validator/null_safety_mixed_mode.dart
@@ -6,7 +6,6 @@
 
 import 'package:path/path.dart' as p;
 
-import '../entrypoint.dart';
 import '../null_safety_analysis.dart';
 import '../package_name.dart';
 import '../source/path.dart';
@@ -15,8 +14,6 @@
 /// Gives a warning when publishing a new version, if this package opts into
 /// null safety, but any of the dependencies do not.
 class NullSafetyMixedModeValidator extends Validator {
-  NullSafetyMixedModeValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
   Future<void> validate() async {
     final pubspec = entrypoint.root.pubspec;
diff --git a/lib/src/validator/pubspec.dart b/lib/src/validator/pubspec.dart
index 2a5e052..ef9cff8 100644
--- a/lib/src/validator/pubspec.dart
+++ b/lib/src/validator/pubspec.dart
@@ -6,7 +6,6 @@
 
 import 'package:path/path.dart' as p;
 
-import '../entrypoint.dart';
 import '../validator.dart';
 
 /// Validates that a package's pubspec exists.
@@ -14,13 +13,10 @@
 /// In most cases this is clearly true, since pub can't run without a pubspec,
 /// but it's possible that the pubspec is gitignored.
 class PubspecValidator extends Validator {
-  PubspecValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
   Future validate() async {
-    var files = entrypoint.root.listFiles(recursive: false);
-    if (!files.any((file) =>
-        p.canonicalize(file) == p.canonicalize(entrypoint.pubspecPath))) {
+    if (!filesBeneath('.', recursive: false)
+        .any((file) => p.basename(file) == 'pubspec.yaml')) {
       errors.add('The pubspec is hidden, probably by .gitignore or pubignore.');
     }
   }
diff --git a/lib/src/validator/pubspec_field.dart b/lib/src/validator/pubspec_field.dart
index 8a5c23e..91d8da3 100644
--- a/lib/src/validator/pubspec_field.dart
+++ b/lib/src/validator/pubspec_field.dart
@@ -4,16 +4,13 @@
 
 import 'dart:async';
 
-import '../entrypoint.dart';
 import '../validator.dart';
 
 /// A validator that checks that the pubspec has valid "author" and "homepage"
 /// fields.
 class PubspecFieldValidator extends Validator {
-  PubspecFieldValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
-  Future validate() {
+  Future<void> validate() {
     _validateFieldIsString('description');
     _validateFieldUrl('homepage');
     _validateFieldUrl('repository');
diff --git a/lib/src/validator/pubspec_typo.dart b/lib/src/validator/pubspec_typo.dart
index 9b04292..0843bc4 100644
--- a/lib/src/validator/pubspec_typo.dart
+++ b/lib/src/validator/pubspec_typo.dart
@@ -2,14 +2,11 @@
 // 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 '../entrypoint.dart';
 import '../levenshtein.dart';
 import '../validator.dart';
 
 /// Validates that a package's pubspec does not contain any typos in its keys.
 class PubspecTypoValidator extends Validator {
-  PubspecTypoValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
   Future validate() async {
     final fields = entrypoint.root.pubspec.fields;
diff --git a/lib/src/validator/readme.dart b/lib/src/validator/readme.dart
index 85de479..3604232 100644
--- a/lib/src/validator/readme.dart
+++ b/lib/src/validator/readme.dart
@@ -5,40 +5,51 @@
 import 'dart:async';
 import 'dart:convert';
 
-import 'package:path/path.dart' as path;
+import 'package:path/path.dart' as p;
 
-import '../entrypoint.dart';
 import '../io.dart';
 import '../validator.dart';
 
+final _readmeRegexp = RegExp(r'^README($|\.)', caseSensitive: false);
+
 /// Validates that a package's README exists and is valid utf-8.
 class ReadmeValidator extends Validator {
-  ReadmeValidator(Entrypoint entrypoint) : super(entrypoint);
-
   @override
-  Future validate() {
-    return Future.sync(() {
-      var readme = entrypoint.root.readmePath;
-      if (readme == null) {
-        warnings
-            .add('Please add a README.md file that describes your package.');
-        return;
-      }
+  Future<void> validate() async {
+    // Find the path to the README file at the root of the entrypoint.
+    //
+    // If multiple READMEs are found, this uses the same conventions as
+    // pub.dev for choosing the primary one: the README with the fewest
+    // extensions that is lexically ordered first is chosen.
+    final readmes = filesBeneath('.', recursive: false)
+        .where((file) => p.basename(file).contains(_readmeRegexp));
 
-      if (path.basename(readme) != 'README.md') {
-        warnings.add('Please consider renaming $readme to `README.md`. '
-            'See https://dart.dev/tools/pub/publishing#important-files.');
-      }
+    if (readmes.isEmpty) {
+      warnings.add('Please add a README.md file that describes your package.');
+      return;
+    }
 
-      var bytes = readBinaryFile(readme);
-      try {
-        // utf8.decode doesn't allow invalid UTF-8.
-        utf8.decode(bytes);
-      } on FormatException catch (_) {
-        warnings.add('$readme contains invalid UTF-8.\n'
-            'This will cause it to be displayed incorrectly on '
-            'the Pub site (https://pub.dev).');
-      }
+    final readme = readmes.reduce((readme1, readme2) {
+      final extensions1 = '.'.allMatches(p.basename(readme1)).length;
+      final extensions2 = '.'.allMatches(p.basename(readme2)).length;
+      var comparison = extensions1.compareTo(extensions2);
+      if (comparison == 0) comparison = readme1.compareTo(readme2);
+      return (comparison <= 0) ? readme1 : readme2;
     });
+
+    if (p.basename(readme) != 'README.md') {
+      warnings.add('Please consider renaming $readme to `README.md`. '
+          'See https://dart.dev/tools/pub/publishing#important-files.');
+    }
+
+    var bytes = readBinaryFile(readme);
+    try {
+      // utf8.decode doesn't allow invalid UTF-8.
+      utf8.decode(bytes);
+    } on FormatException catch (_) {
+      warnings.add('$readme contains invalid UTF-8.\n'
+          'This will cause it to be displayed incorrectly on '
+          'the Pub site (https://pub.dev).');
+    }
   }
 }
diff --git a/lib/src/validator/relative_version_numbering.dart b/lib/src/validator/relative_version_numbering.dart
index 14c7334..1b99b79 100644
--- a/lib/src/validator/relative_version_numbering.dart
+++ b/lib/src/validator/relative_version_numbering.dart
@@ -6,7 +6,6 @@
 
 import 'package:collection/collection.dart' show IterableExtension;
 
-import '../entrypoint.dart';
 import '../exceptions.dart';
 import '../null_safety_analysis.dart';
 import '../package_name.dart';
@@ -18,18 +17,13 @@
   static const String semverUrl =
       'https://dart.dev/tools/pub/versioning#semantic-versions';
 
-  final Uri? _server;
-
-  RelativeVersionNumberingValidator(Entrypoint entrypoint, this._server)
-      : super(entrypoint);
-
   @override
   Future<void> validate() async {
     final hostedSource = entrypoint.cache.hosted;
     List<PackageId> existingVersions;
     try {
       existingVersions = await entrypoint.cache.getVersions(
-        hostedSource.refFor(entrypoint.root.name, url: _server.toString()),
+        hostedSource.refFor(entrypoint.root.name, url: serverUrl.toString()),
       );
     } on PackageNotFoundException {
       existingVersions = [];
diff --git a/lib/src/validator/sdk_constraint.dart b/lib/src/validator/sdk_constraint.dart
index ff25e53..736876b 100644
--- a/lib/src/validator/sdk_constraint.dart
+++ b/lib/src/validator/sdk_constraint.dart
@@ -6,7 +6,6 @@
 
 import 'package:pub_semver/pub_semver.dart';
 
-import '../entrypoint.dart';
 import '../sdk.dart';
 import '../validator.dart';
 
@@ -18,8 +17,6 @@
 /// * is not depending on a prerelease, unless the package itself is a
 /// prerelease.
 class SdkConstraintValidator extends Validator {
-  SdkConstraintValidator(Entrypoint entrypoint) : super(entrypoint);
-
   /// Get SDK version constraint from `pubspec.yaml` without any defaults or
   /// overrides.
   VersionConstraint _sdkConstraintFromPubspecYaml() {
diff --git a/lib/src/validator/size.dart b/lib/src/validator/size.dart
index e989518..ae32496 100644
--- a/lib/src/validator/size.dart
+++ b/lib/src/validator/size.dart
@@ -3,9 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:math' as math;
 
-import '../entrypoint.dart';
 import '../io.dart';
 import '../validator.dart';
 
@@ -14,30 +12,24 @@
 
 /// A validator that validates that a package isn't too big.
 class SizeValidator extends Validator {
-  final Future<int> packageSize;
-
-  SizeValidator(Entrypoint entrypoint, this.packageSize) : super(entrypoint);
-
   @override
-  Future validate() {
-    return packageSize.then((size) {
-      if (size <= _maxSize) return;
-      var sizeInMb = (size / math.pow(2, 20)).toStringAsPrecision(4);
-      // Current implementation of Package.listFiles skips hidden files
-      var ignoreExists = fileExists(entrypoint.root.path('.gitignore'));
+  Future<void> validate() async {
+    if (packageSize <= _maxSize) return;
+    var sizeInMb = (packageSize / (1 << 20)).toStringAsPrecision(4);
+    // Current implementation of Package.listFiles skips hidden files
+    var ignoreExists = fileExists(entrypoint.root.path('.gitignore'));
 
-      var error = StringBuffer('Your package is $sizeInMb MB. Hosted '
-          'packages must be smaller than 100 MB.');
+    var error = StringBuffer('Your package is $sizeInMb MB. Hosted '
+        'packages must be smaller than 100 MB.');
 
-      if (ignoreExists && !entrypoint.root.inGitRepo) {
-        error.write(' Your .gitignore has no effect since your project '
-            'does not appear to be in version control.');
-      } else if (!ignoreExists && entrypoint.root.inGitRepo) {
-        error.write(' Consider adding a .gitignore to avoid including '
-            'temporary files.');
-      }
+    if (ignoreExists && !entrypoint.root.inGitRepo) {
+      error.write(' Your .gitignore has no effect since your project '
+          'does not appear to be in version control.');
+    } else if (!ignoreExists && entrypoint.root.inGitRepo) {
+      error.write(' Consider adding a .gitignore to avoid including '
+          'temporary files.');
+    }
 
-      errors.add(error.toString());
-    });
+    errors.add(error.toString());
   }
 }
diff --git a/lib/src/validator/strict_dependencies.dart b/lib/src/validator/strict_dependencies.dart
index 881c55c..84f7a09 100644
--- a/lib/src/validator/strict_dependencies.dart
+++ b/lib/src/validator/strict_dependencies.dart
@@ -11,7 +11,6 @@
 import 'package:stack_trace/stack_trace.dart';
 
 import '../dart.dart';
-import '../entrypoint.dart';
 import '../io.dart';
 import '../log.dart' as log;
 import '../utils.dart';
@@ -19,19 +18,16 @@
 
 /// Validates that Dart source files only import declared dependencies.
 class StrictDependenciesValidator extends Validator {
-  final AnalysisContextManager analysisContextManager =
-      AnalysisContextManager();
-
-  StrictDependenciesValidator(Entrypoint entrypoint) : super(entrypoint) {
-    var packagePath = p.normalize(p.absolute(entrypoint.root.dir));
-    analysisContextManager.createContextsForDirectory(packagePath);
-  }
-
   /// Lazily returns all dependency uses in [files].
   ///
   /// Files that do not parse and directives that don't import or export
   /// `package:` URLs are ignored.
   Iterable<_Usage> _findPackages(Iterable<String> files) sync* {
+    final packagePath = p.normalize(p.absolute(entrypoint.root.dir));
+    final AnalysisContextManager analysisContextManager =
+        AnalysisContextManager();
+    analysisContextManager.createContextsForDirectory(packagePath);
+
     for (var file in files) {
       List<UriBasedDirective> directives;
       var contents = readTextFile(file);
@@ -105,10 +101,16 @@
     }
   }
 
-  Iterable<_Usage> _usagesBeneath(List<String> paths) => _findPackages(paths
-      .map((path) => entrypoint.root.listFiles(beneath: path))
-      .expand((files) => files)
-      .where((String file) => p.extension(file) == '.dart'));
+  Iterable<_Usage> _usagesBeneath(List<String> paths) {
+    return _findPackages(
+      paths.expand(
+        (path) {
+          return filesBeneath(path, recursive: true)
+              .where((file) => p.extension(file) == '.dart');
+        },
+      ),
+    );
+  }
 }
 
 /// A parsed import or export directive in a D source file.
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 216dbe0..10b2a76 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -839,14 +839,23 @@
 }
 
 /// A function that creates a [Validator] subclass.
-typedef ValidatorCreator = Validator Function(Entrypoint entrypoint);
+typedef ValidatorCreator = Validator Function();
 
 /// Schedules a single [Validator] to run on the [appPath].
 ///
 /// Returns a scheduled Future that contains the validator after validation.
-Future<Validator> validatePackage(ValidatorCreator fn) async {
+Future<Validator> validatePackage(ValidatorCreator fn, int? size) async {
   var cache = SystemCache(rootDir: _pathInSandbox(cachePath));
-  var validator = fn(Entrypoint(_pathInSandbox(appPath), cache));
+  final entrypoint = Entrypoint(_pathInSandbox(appPath), cache);
+  var validator = fn();
+  validator.context = ValidationContext(
+    entrypoint,
+    await Future.value(size ?? 100),
+    _globalServer == null
+        ? Uri.parse('https://pub.dev')
+        : Uri.parse(globalServer.url),
+    entrypoint.root.listFiles(),
+  );
   await validator.validate();
   return validator;
 }
diff --git a/test/validator/changelog_test.dart b/test/validator/changelog_test.dart
index 5bef662..397723d 100644
--- a/test/validator/changelog_test.dart
+++ b/test/validator/changelog_test.dart
@@ -2,7 +2,6 @@
 // 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:pub/src/entrypoint.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/changelog.dart';
 import 'package:test/test.dart';
@@ -11,7 +10,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator changelog(Entrypoint entrypoint) => ChangelogValidator(entrypoint);
+Validator changelog() => ChangelogValidator();
 
 void main() {
   group('should consider a package valid if it', () {
diff --git a/test/validator/compiled_dartdoc_test.dart b/test/validator/compiled_dartdoc_test.dart
index f1d3d6d..da02005 100644
--- a/test/validator/compiled_dartdoc_test.dart
+++ b/test/validator/compiled_dartdoc_test.dart
@@ -2,7 +2,6 @@
 // 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:pub/src/entrypoint.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/compiled_dartdoc.dart';
 import 'package:test/test.dart';
@@ -11,8 +10,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator compiledDartdoc(Entrypoint entrypoint) =>
-    CompiledDartdocValidator(entrypoint);
+Validator compiledDartdoc() => CompiledDartdocValidator();
 
 void main() {
   setUp(d.validPackage.create);
diff --git a/test/validator/dependency_override_test.dart b/test/validator/dependency_override_test.dart
index 21a437d..b801ad9 100644
--- a/test/validator/dependency_override_test.dart
+++ b/test/validator/dependency_override_test.dart
@@ -2,7 +2,6 @@
 // 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:pub/src/entrypoint.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/dependency_override.dart';
 import 'package:test/test.dart';
@@ -11,8 +10,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator dependencyOverride(Entrypoint entrypoint) =>
-    DependencyOverrideValidator(entrypoint);
+Validator dependencyOverride() => DependencyOverrideValidator();
 
 void main() {
   test(
diff --git a/test/validator/dependency_test.dart b/test/validator/dependency_test.dart
index 5a6380b..f90ed1d 100644
--- a/test/validator/dependency_test.dart
+++ b/test/validator/dependency_test.dart
@@ -6,6 +6,7 @@
 import 'dart:convert';
 
 import 'package:path/path.dart' as path;
+
 import 'package:pub/src/exit_codes.dart';
 import 'package:test/test.dart';
 
diff --git a/test/validator/deprecated_fields_test.dart b/test/validator/deprecated_fields_test.dart
index ee4551d..65f3ca0 100644
--- a/test/validator/deprecated_fields_test.dart
+++ b/test/validator/deprecated_fields_test.dart
@@ -2,7 +2,6 @@
 // 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:pub/src/entrypoint.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/deprecated_fields.dart';
 import 'package:test/test.dart';
@@ -11,8 +10,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator deprecatedFields(Entrypoint entrypoint) =>
-    DeprecatedFieldsValidator(entrypoint);
+Validator deprecatedFields() => DeprecatedFieldsValidator();
 
 void main() {
   setUp(d.validPackage.create);
diff --git a/test/validator/directory_test.dart b/test/validator/directory_test.dart
index 88fa80a..1ccebde 100644
--- a/test/validator/directory_test.dart
+++ b/test/validator/directory_test.dart
@@ -2,7 +2,6 @@
 // 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:pub/src/entrypoint.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/directory.dart';
 import 'package:test/test.dart';
@@ -11,7 +10,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator directory(Entrypoint entrypoint) => DirectoryValidator(entrypoint);
+Validator directory() => DirectoryValidator();
 
 void main() {
   group('should consider a package valid if it', () {
diff --git a/test/validator/executable_test.dart b/test/validator/executable_test.dart
index d9973e7..c1f2a50 100644
--- a/test/validator/executable_test.dart
+++ b/test/validator/executable_test.dart
@@ -2,7 +2,6 @@
 // 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:pub/src/entrypoint.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/executable.dart';
 import 'package:test/test.dart';
@@ -11,7 +10,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator executable(Entrypoint entrypoint) => ExecutableValidator(entrypoint);
+Validator executable() => ExecutableValidator();
 
 void main() {
   setUp(d.validPackage.create);
diff --git a/test/validator/flutter_plugin_format_test.dart b/test/validator/flutter_plugin_format_test.dart
index 0c24bda..0a5b28c 100644
--- a/test/validator/flutter_plugin_format_test.dart
+++ b/test/validator/flutter_plugin_format_test.dart
@@ -2,7 +2,6 @@
 // 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:pub/src/entrypoint.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/flutter_plugin_format.dart';
 import 'package:test/test.dart';
@@ -11,8 +10,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator flutterPluginFormat(Entrypoint entrypoint) =>
-    FlutterPluginFormatValidator(entrypoint);
+Validator flutterPluginFormat() => FlutterPluginFormatValidator();
 
 void main() {
   group('should consider a package valid if it', () {
diff --git a/test/validator/language_version_test.dart b/test/validator/language_version_test.dart
index 39c9465..fc2351e 100644
--- a/test/validator/language_version_test.dart
+++ b/test/validator/language_version_test.dart
@@ -2,7 +2,6 @@
 // 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:pub/src/entrypoint.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/language_version.dart';
 import 'package:test/test.dart';
@@ -11,8 +10,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator validator(Entrypoint entrypoint) =>
-    LanguageVersionValidator(entrypoint);
+Validator validator() => LanguageVersionValidator();
 
 Future<void> setup(
     {required String sdkConstraint, String? libraryLanguageVersion}) async {
diff --git a/test/validator/leak_detection_test.dart b/test/validator/leak_detection_test.dart
index c055e59..fc64c68 100644
--- a/test/validator/leak_detection_test.dart
+++ b/test/validator/leak_detection_test.dart
@@ -2,7 +2,6 @@
 // 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:pub/src/entrypoint.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/leak_detection.dart';
 import 'package:test/test.dart';
@@ -11,8 +10,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator leakDetection(Entrypoint entrypoint) =>
-    LeakDetectionValidator(entrypoint);
+Validator leakDetection() => LeakDetectionValidator();
 
 void main() {
   group('should consider a package valid if it', () {
diff --git a/test/validator/license_test.dart b/test/validator/license_test.dart
index 8717c75..17aeec5 100644
--- a/test/validator/license_test.dart
+++ b/test/validator/license_test.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:path/path.dart' as path;
-import 'package:pub/src/entrypoint.dart';
 import 'package:pub/src/io.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/license.dart';
@@ -13,7 +12,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator license(Entrypoint entrypoint) => LicenseValidator(entrypoint);
+Validator license() => LicenseValidator();
 
 void main() {
   group('should consider a package valid if it', () {
diff --git a/test/validator/name_test.dart b/test/validator/name_test.dart
index 7cbb51c..2e8f8af 100644
--- a/test/validator/name_test.dart
+++ b/test/validator/name_test.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:path/path.dart' as path;
-import 'package:pub/src/entrypoint.dart';
 import 'package:pub/src/io.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/name.dart';
@@ -13,7 +12,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator name(Entrypoint entrypoint) => NameValidator(entrypoint);
+Validator name() => NameValidator();
 
 void main() {
   group('should consider a package valid if it', () {
diff --git a/test/validator/pubspec_field_test.dart b/test/validator/pubspec_field_test.dart
index 8e6fefc..43db483 100644
--- a/test/validator/pubspec_field_test.dart
+++ b/test/validator/pubspec_field_test.dart
@@ -2,7 +2,6 @@
 // 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:pub/src/entrypoint.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/pubspec_field.dart';
 import 'package:test/test.dart';
@@ -11,8 +10,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator pubspecField(Entrypoint entrypoint) =>
-    PubspecFieldValidator(entrypoint);
+Validator pubspecField() => PubspecFieldValidator();
 
 void main() {
   group('should consider a package valid if it', () {
diff --git a/test/validator/pubspec_test.dart b/test/validator/pubspec_test.dart
index 9942220..ea0f5af 100644
--- a/test/validator/pubspec_test.dart
+++ b/test/validator/pubspec_test.dart
@@ -13,7 +13,7 @@
   test('should consider a package valid if it has a pubspec', () async {
     await d.validPackage.create();
 
-    await expectValidation((entrypoint) => PubspecValidator(entrypoint));
+    await expectValidation(() => PubspecValidator());
   });
 
   test('should consider a package invalid if it has a .gitignored pubspec',
@@ -22,7 +22,6 @@
     await d.validPackage.create();
     await repo.create();
 
-    await expectValidation((entrypoint) => PubspecValidator(entrypoint),
-        errors: isNotEmpty);
+    await expectValidation(() => PubspecValidator(), errors: isNotEmpty);
   });
 }
diff --git a/test/validator/pubspec_typo_test.dart b/test/validator/pubspec_typo_test.dart
index ab2a547..bbe7db3 100644
--- a/test/validator/pubspec_typo_test.dart
+++ b/test/validator/pubspec_typo_test.dart
@@ -2,7 +2,6 @@
 // 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:pub/src/entrypoint.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/pubspec_typo.dart';
 import 'package:test/test.dart';
@@ -11,8 +10,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator pubspecTypo(Entrypoint entrypoint) =>
-    PubspecTypoValidator(entrypoint);
+Validator pubspecTypo() => PubspecTypoValidator();
 
 void main() {
   group('should consider a package valid if it', () {
diff --git a/test/validator/readme_test.dart b/test/validator/readme_test.dart
index 6519c6f..17b0602 100644
--- a/test/validator/readme_test.dart
+++ b/test/validator/readme_test.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:path/path.dart' as p;
-import 'package:pub/src/entrypoint.dart';
 import 'package:pub/src/io.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/readme.dart';
@@ -13,7 +12,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator readme(Entrypoint entrypoint) => ReadmeValidator(entrypoint);
+Validator readme() => ReadmeValidator();
 
 void main() {
   setUp(d.validPackage.create);
diff --git a/test/validator/relative_version_numbering_test.dart b/test/validator/relative_version_numbering_test.dart
index 2f3da5c..9a8bca0 100644
--- a/test/validator/relative_version_numbering_test.dart
+++ b/test/validator/relative_version_numbering_test.dart
@@ -2,7 +2,6 @@
 // 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:pub/src/entrypoint.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/relative_version_numbering.dart';
 import 'package:test/test.dart';
@@ -11,10 +10,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator validator(Entrypoint entrypoint) => RelativeVersionNumberingValidator(
-      entrypoint,
-      Uri.parse(globalServer.url),
-    );
+Validator validator() => RelativeVersionNumberingValidator();
 
 Future<void> setup({required String sdkConstraint}) async {
   await d.validPackage.create();
diff --git a/test/validator/sdk_constraint_test.dart b/test/validator/sdk_constraint_test.dart
index c24adf2..7e82aef 100644
--- a/test/validator/sdk_constraint_test.dart
+++ b/test/validator/sdk_constraint_test.dart
@@ -2,7 +2,6 @@
 // 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:pub/src/entrypoint.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/sdk_constraint.dart';
 import 'package:test/test.dart';
@@ -11,8 +10,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator sdkConstraint(Entrypoint entrypoint) =>
-    SdkConstraintValidator(entrypoint);
+Validator sdkConstraint() => SdkConstraintValidator();
 
 void main() {
   group('should consider a package valid if it', () {
diff --git a/test/validator/size_test.dart b/test/validator/size_test.dart
index 3a22e60..ba5151d 100644
--- a/test/validator/size_test.dart
+++ b/test/validator/size_test.dart
@@ -11,13 +11,10 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-ValidatorCreator size(int size) {
-  return (entrypoint) => SizeValidator(entrypoint, Future.value(size));
-}
-
 Future<void> expectSizeValidationError(Matcher matcher) async {
   await expectValidation(
-    size(100 * 1048577 /*2^20 +1*/),
+    () => SizeValidator(),
+    size: 100 * (1 << 20) + 1,
     errors: contains(matcher),
   );
 }
@@ -26,8 +23,8 @@
   test('considers a package valid if it is <= 100 MB', () async {
     await d.validPackage.create();
 
-    await expectValidation(size(100));
-    await expectValidation(size(100 * 1048576 /*2^20*/));
+    await expectValidation(() => SizeValidator(), size: 100);
+    await expectValidation(() => SizeValidator(), size: 100 * (1 << 20));
   });
 
   group('considers a package invalid if it is more than 100 MB', () {
diff --git a/test/validator/strict_dependencies_test.dart b/test/validator/strict_dependencies_test.dart
index 7b7962a..a6b0833 100644
--- a/test/validator/strict_dependencies_test.dart
+++ b/test/validator/strict_dependencies_test.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:path/path.dart' as path;
-import 'package:pub/src/entrypoint.dart';
 import 'package:pub/src/validator.dart';
 import 'package:pub/src/validator/strict_dependencies.dart';
 import 'package:test/test.dart';
@@ -12,8 +11,7 @@
 import '../test_pub.dart';
 import 'utils.dart';
 
-Validator strictDeps(Entrypoint entrypoint) =>
-    StrictDependenciesValidator(entrypoint);
+Validator strictDeps() => StrictDependenciesValidator();
 
 void main() {
   group('should consider a package valid if it', () {
diff --git a/test/validator/utils.dart b/test/validator/utils.dart
index 5123683..8a1c0e2 100644
--- a/test/validator/utils.dart
+++ b/test/validator/utils.dart
@@ -10,8 +10,8 @@
 // That would make them more robust, and test actual end2end behaviour.
 
 Future<void> expectValidation(ValidatorCreator fn,
-    {hints, warnings, errors}) async {
-  final validator = await validatePackage(fn);
+    {hints, warnings, errors, int? size}) async {
+  final validator = await validatePackage(fn, size);
   expect(validator.errors, errors ?? isEmpty);
   expect(validator.warnings, warnings ?? isEmpty);
   expect(validator.hints, hints ?? isEmpty);