| // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:async'; |
| |
| import 'package:meta/meta.dart'; |
| import 'package:path/path.dart' as p; |
| import 'package:pub_semver/pub_semver.dart'; |
| |
| import 'entrypoint.dart'; |
| import 'log.dart' as log; |
| import 'package.dart'; |
| import 'sdk.dart'; |
| import 'system_cache.dart'; |
| import 'validator/analyze.dart'; |
| import 'validator/changelog.dart'; |
| import 'validator/compiled_dartdoc.dart'; |
| import 'validator/dependency.dart'; |
| import 'validator/dependency_override.dart'; |
| import 'validator/deprecated_fields.dart'; |
| import 'validator/devtools_extension.dart'; |
| import 'validator/directory.dart'; |
| import 'validator/executable.dart'; |
| import 'validator/file_case.dart'; |
| import 'validator/flutter_constraint.dart'; |
| import 'validator/flutter_plugin_format.dart'; |
| import 'validator/git_status.dart'; |
| import 'validator/gitignore.dart'; |
| import 'validator/leak_detection.dart'; |
| import 'validator/license.dart'; |
| import 'validator/name.dart'; |
| import 'validator/pubspec.dart'; |
| import 'validator/pubspec_field.dart'; |
| import 'validator/pubspec_typo.dart'; |
| import 'validator/readme.dart'; |
| import 'validator/relative_version_numbering.dart'; |
| import 'validator/sdk_constraint.dart'; |
| import 'validator/size.dart'; |
| import 'validator/strict_dependencies.dart'; |
| |
| /// The base class for validators that check whether a package is fit for |
| /// uploading. |
| /// |
| /// Each validator should override [errors], [warnings], or both to return |
| /// lists of errors or warnings to display to the user. Errors will cause the |
| /// package not to be uploaded; warnings will require the user to confirm the |
| /// upload. |
| abstract class Validator { |
| /// The accumulated errors for this validator. |
| /// |
| /// Filled by calling [validate]. |
| final errors = <String>[]; |
| |
| /// The accumulated warnings for this validator. |
| /// |
| /// Filled by calling [validate]. |
| final warnings = <String>[]; |
| |
| /// The accumulated hints for this validator. |
| /// |
| /// Filled by calling [validate]. |
| final hints = <String>[]; |
| |
| late ValidationContext context; |
| Package get package => context.entrypoint.workPackage; |
| SystemCache get cache => context.entrypoint.cache; |
| 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<void> validate(); |
| |
| /// Adds an error if the package's SDK constraint doesn't exclude Dart SDK |
| /// versions older than [firstSdkVersion]. |
| @protected |
| void validateSdkConstraint(Version firstSdkVersion, String message) { |
| // If the SDK constraint disallowed all versions before [firstSdkVersion], |
| // no error is necessary. |
| if (package.pubspec.dartSdkConstraint.originalConstraint |
| .intersect(VersionRange(max: firstSdkVersion)) |
| .isEmpty) { |
| return; |
| } |
| |
| if (firstSdkVersion.isPreRelease && |
| !_isSamePreRelease(firstSdkVersion, sdk.version)) { |
| // Unless the user is using a dev SDK themselves, suggest that they use a |
| // non-dev SDK constraint, even if there were some dev versions that are |
| // allowed. |
| firstSdkVersion = firstSdkVersion.nextPatch; |
| } |
| |
| final allowedSdks = VersionRange( |
| min: firstSdkVersion, |
| includeMin: true, |
| max: |
| firstSdkVersion.isPreRelease |
| ? firstSdkVersion.nextPatch |
| : firstSdkVersion.nextBreaking, |
| ); |
| |
| var newSdkConstraint = package.pubspec.dartSdkConstraint.originalConstraint |
| .intersect(allowedSdks); |
| if (newSdkConstraint.isEmpty) newSdkConstraint = allowedSdks; |
| |
| errors.add( |
| '$message\n' |
| 'Make sure your SDK constraint excludes old versions:\n' |
| '\n' |
| 'environment:\n' |
| ' sdk: "${newSdkConstraint.asCompatibleWithIfPossible()}"', |
| ); |
| } |
| |
| /// Returns whether [version1] and [version2] are pre-releases of the same |
| /// version. |
| bool _isSamePreRelease(Version version1, Version version2) => |
| version1.isPreRelease && |
| version2.isPreRelease && |
| version1.patch == version2.patch && |
| version1.minor == version2.minor && |
| version1.major == version2.major; |
| |
| /// 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, |
| int packageSize, |
| Uri serverUrl, |
| List<String> files, { |
| required List<String> hints, |
| required List<String> warnings, |
| required List<String> errors, |
| }) async { |
| final validators = [ |
| FileCaseValidator(), |
| AnalyzeValidator(), |
| GitignoreValidator(), |
| GitStatusValidator(), |
| PubspecValidator(), |
| LicenseValidator(), |
| NameValidator(), |
| PubspecFieldValidator(), |
| DependencyValidator(), |
| DependencyOverrideValidator(), |
| DeprecatedFieldsValidator(), |
| DirectoryValidator(), |
| ExecutableValidator(), |
| CompiledDartdocValidator(), |
| ReadmeValidator(), |
| ChangelogValidator(), |
| SdkConstraintValidator(), |
| StrictDependenciesValidator(), |
| FlutterConstraintValidator(), |
| FlutterPluginFormatValidator(), |
| RelativeVersionNumberingValidator(), |
| PubspecTypoValidator(), |
| LeakDetectionValidator(), |
| SizeValidator(), |
| DevtoolsExtensionValidator(), |
| ]; |
| |
| final context = ValidationContext( |
| entrypoint, |
| 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, |
| ]); |
| errors.addAll([for (final validator in validators) ...validator.errors]); |
| |
| String presentDiagnostics(List<String> diagnostics) => diagnostics |
| .map((diagnostic) => "* ${diagnostic.split('\n').join('\n ')}\n") |
| .join('\n'); |
| final sections = <String>[]; |
| |
| for (final (kind, diagnostics) in [ |
| ('error', errors), |
| ('potential issue', warnings), |
| ('hint', hints), |
| ]) { |
| if (diagnostics.isNotEmpty) { |
| final s = diagnostics.length > 1 ? 's' : ''; |
| final count = diagnostics.length > 1 ? '${diagnostics.length} ' : ''; |
| sections.add( |
| 'Package validation found the following $count$kind$s:\n' |
| '${presentDiagnostics(diagnostics)}', |
| ); |
| } |
| } |
| log.message(sections.join('\n')); |
| }); |
| } |
| |
| /// Returns the [files] that are [path] or inside [path] (relative to the |
| /// package entrypoint). |
| // TODO(sigurdm): Consider moving this to a more central location. |
| List<String> filesBeneath(String path, {required bool recursive}) { |
| final base = p.canonicalize(p.join(package.dir, path)); |
| return files |
| .where( |
| recursive |
| ? (file) => |
| p.isWithin(base, p.canonicalize(file)) || |
| p.canonicalize(file) == 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, |
| ); |
| } |