blob: 8bc35d00a2ea62c96e7583ce70ce3c75a5892eaf [file] [log] [blame]
// 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();
}