| // Copyright (c) 2024, 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:io'; |
| |
| import 'config.dart'; |
| import 'encoded_asset.dart'; |
| import 'extension.dart'; |
| import 'hooks/syntax.g.dart'; |
| |
| /// A list of [String]s describing validation errors. |
| typedef ValidationErrors = List<String>; |
| |
| /// The base protocol for `hook/build.dart` and `hook/link.dart` which can be |
| /// extended with [ProtocolExtension]s. |
| /// |
| /// This class contains the [HookInput] and [HookOutput] validation rules for |
| /// the base protocol. (In contrast to [ProtocolExtension]s, it does not contain |
| /// setup methods for the [HookInput], the base protocol knows how to setup |
| /// itself.) |
| class ProtocolBase { |
| /// Validates the [BuildInput] for both syntax and semantics. |
| static Future<ValidationErrors> validateBuildInput(BuildInput input) async { |
| final syntaxErrors = BuildInputSyntax.fromJson(input.json).validate(); |
| if (syntaxErrors.isNotEmpty) { |
| return [...syntaxErrors, _semanticValidationSkippedMessage]; |
| } |
| |
| return _validateHookInput('BuildInput', input); |
| } |
| |
| /// Validates the [LinkInput] for both syntax and semantics. |
| static Future<ValidationErrors> validateLinkInput(LinkInput input) async { |
| final syntaxErrors = LinkInputSyntax.fromJson(input.json).validate(); |
| if (syntaxErrors.isNotEmpty) { |
| return [...syntaxErrors, _semanticValidationSkippedMessage]; |
| } |
| |
| final recordUses = input.recordedUsagesFile; |
| return <String>[ |
| ..._validateHookInput('LinkInput', input), |
| if (recordUses != null) |
| ..._validateDirectory( |
| '$LinkInput.recordUses', |
| input.outputDirectoryShared, |
| ), |
| ]; |
| } |
| |
| static ValidationErrors _validateHookInput( |
| String inputName, |
| HookInput input, |
| ) { |
| final errors = <String>[ |
| ..._validateDirectory('$inputName.packageRoot', input.packageRoot), |
| ..._validateDirectory( |
| '$inputName.outputDirectory', |
| input.outputDirectory, |
| ), |
| ..._validateDirectory( |
| '$inputName.outputDirectoryShared', |
| input.outputDirectoryShared, |
| ), |
| ..._validateDirectory( |
| '$inputName.outputFile', |
| input.outputFile, |
| mustExist: false, |
| ), |
| ]; |
| return errors; |
| } |
| |
| static ValidationErrors _validateDirectory( |
| String name, |
| Uri uri, { |
| bool mustExist = true, |
| bool mustBeAbsolute = true, |
| }) { |
| final errors = <String>[]; |
| if (mustBeAbsolute && !uri.isAbsolute) { |
| errors.add('$name (${uri.toFilePath()}) must be an absolute path.'); |
| } |
| if (mustExist && !Directory.fromUri(uri).existsSync()) { |
| errors.add('$name (${uri.toFilePath()}) does not exist as a directory.'); |
| } |
| return errors; |
| } |
| |
| /// Validates the [BuildOutput] for both syntax and semantics. |
| static Future<ValidationErrors> validateBuildOutput( |
| BuildInput input, |
| BuildOutput output, |
| ) async { |
| final syntaxErrors = BuildOutputSyntax.fromJson(output.json).validate(); |
| if (syntaxErrors.isNotEmpty) { |
| return [...syntaxErrors, _semanticValidationSkippedMessage]; |
| } |
| |
| final errors = [ |
| ..._validateAssetsForLinking(input, output), |
| ..._validateOutputAssetTypes(input, output.assets.encodedAssets), |
| ]; |
| if (input.config.linkingEnabled) { |
| for (final assets in output.assets.encodedAssetsForLinking.values) { |
| errors.addAll(_validateOutputAssetTypes(input, assets)); |
| } |
| } |
| return errors; |
| } |
| |
| /// Validates the [LinkOutput] for both syntax and semantics. |
| static Future<ValidationErrors> validateLinkOutput( |
| LinkInput input, |
| LinkOutput output, |
| ) async { |
| final syntaxErrors = LinkOutputSyntax.fromJson(output.json).validate(); |
| if (syntaxErrors.isNotEmpty) { |
| return [...syntaxErrors, _semanticValidationSkippedMessage]; |
| } |
| |
| final errors = [ |
| ..._validateOutputAssetTypes(input, output.assets.encodedAssets), |
| ]; |
| return errors; |
| } |
| |
| /// Only output asset types that are supported by the embedder. |
| static ValidationErrors _validateOutputAssetTypes( |
| HookInput input, |
| Iterable<EncodedAsset> assets, |
| ) { |
| final errors = <String>[]; |
| final List<String> buildAssetTypes; |
| if (input is BuildInput) { |
| buildAssetTypes = input.config.buildAssetTypes; |
| } else { |
| buildAssetTypes = (input as LinkInput).config.buildAssetTypes; |
| } |
| for (final asset in assets) { |
| if (!buildAssetTypes.contains(asset.type)) { |
| final error = |
| 'Asset with type "${asset.type}" is not a supported asset type ' |
| '(${buildAssetTypes.join(' ')} are supported)'; |
| errors.add(error); |
| } |
| } |
| return errors; |
| } |
| |
| /// EncodedAssetsForLinking should be empty if linking is not supported. |
| static ValidationErrors _validateAssetsForLinking( |
| BuildInput input, |
| BuildOutput output, |
| ) { |
| final errors = <String>[]; |
| if (!input.config.linkingEnabled) { |
| if (output.assets.encodedAssetsForLinking.isNotEmpty) { |
| const error = |
| 'BuildOutput.assets_for_linking is not empty while ' |
| 'BuildInput.config.linkingEnabled is false'; |
| errors.add(error); |
| } |
| } |
| return errors; |
| } |
| |
| static const _semanticValidationSkippedMessage = |
| 'Syntax errors. Semantic validation skipped.'; |
| } |
| |
| /// A test failure. |
| /// |
| /// This cannot be `package:test`s `TestFailure` because we want to avoid |
| /// having a dependency on `package:test`. |
| /// |
| /// Not exported, because this is not meant to be caught. |
| final class ValidationFailure implements Exception { |
| /// The message describing the validation failure. |
| final String? message; |
| |
| /// Creates a new [ValidationFailure] with an optional [message]. |
| ValidationFailure(this.message); |
| |
| @override |
| String toString() => message.toString(); |
| } |