add parsing of assets transformer declarations in pubspec.yaml (#143557)

In service of https://github.com/flutter/flutter/issues/143348.

This PR enables parsing of the pubspec yaml schemes for assets with transformations as described in #143348.
This commit is contained in:
Andrew Kolos 2024-02-16 14:24:59 -08:00 committed by GitHub
parent 848aa5080b
commit 3a18473bd6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 330 additions and 34 deletions

View file

@ -519,7 +519,8 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) {
_validateFonts(yamlValue, errors);
}
case 'licenses':
errors.addAll(_validateList<String>(yamlValue, '"$yamlKey"', 'files'));
final (_, List<String> filesErrors) = _parseList<String>(yamlValue, '"$yamlKey"', 'files');
errors.addAll(filesErrors);
case 'module':
if (yamlValue is! YamlMap) {
errors.add('Expected "$yamlKey" to be an object, but got $yamlValue (${yamlValue.runtimeType}).');
@ -553,11 +554,12 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) {
}
}
List<String> _validateList<T>(Object? yamlList, String context, String typeAlias) {
(List<T>? result, List<String> errors) _parseList<T>(Object? yamlList, String context, String typeAlias) {
final List<String> errors = <String>[];
if (yamlList is! YamlList) {
return <String>['Expected $context to be a list of $typeAlias, but got $yamlList (${yamlList.runtimeType}).'];
final String message = 'Expected $context to be a list of $typeAlias, but got $yamlList (${yamlList.runtimeType}).';
return (null, <String>[message]);
}
for (int i = 0; i < yamlList.length; i++) {
@ -567,8 +569,9 @@ List<String> _validateList<T>(Object? yamlList, String context, String typeAlias
}
}
return errors;
return errors.isEmpty ? (List<T>.from(yamlList), errors) : (null, errors);
}
void _validateDeferredComponents(MapEntry<Object?, Object?> kvp, List<String> errors) {
final Object? yamlList = kvp.value;
if (yamlList != null && (yamlList is! YamlList || yamlList[0] is! YamlMap)) {
@ -585,11 +588,12 @@ void _validateDeferredComponents(MapEntry<Object?, Object?> kvp, List<String> er
errors.add('Expected the $i element in "${kvp.key}" to have required key "name" of type String');
}
if (valueMap.containsKey('libraries')) {
errors.addAll(_validateList<String>(
final (_, List<String> librariesErrors) = _parseList<String>(
valueMap['libraries'],
'"libraries" key in the element at index $i of "${kvp.key}"',
'String',
));
);
errors.addAll(librariesErrors);
}
if (valueMap.containsKey('assets')) {
errors.addAll(_validateAssets(valueMap['assets']));
@ -697,13 +701,16 @@ class AssetsEntry {
const AssetsEntry({
required this.uri,
this.flavors = const <String>{},
this.transformers = const <AssetTransformerEntry>[],
});
final Uri uri;
final Set<String> flavors;
final List<AssetTransformerEntry> transformers;
static const String _pathKey = 'path';
static const String _flavorKey = 'flavors';
static const String _transformersKey = 'transformers';
static AssetsEntry? parseFromYaml(Object? yaml) {
final (AssetsEntry? value, String? error) = parseFromYamlSafe(yaml);
@ -738,7 +745,6 @@ class AssetsEntry {
}
final Object? path = yaml[_pathKey];
final Object? flavors = yaml[_flavorKey];
if (path == null || path is! String) {
return (null, 'Asset manifest entry is malformed. '
@ -746,41 +752,76 @@ class AssetsEntry {
'containing a "$_pathKey" entry. Got ${path.runtimeType} instead.');
}
final Uri uri = Uri(pathSegments: path.split('/'));
final (List<String>? flavors, List<String> flavorsErrors) = _parseFlavorsSection(yaml[_flavorKey]);
final (List<AssetTransformerEntry>? transformers, List<String> transformersErrors) = _parseTransformersSection(yaml[_transformersKey]);
if (flavors == null) {
return (AssetsEntry(uri: uri), null);
final List<String> errors = <String>[
...flavorsErrors.map((String e) => 'In $_flavorKey section of asset "$path": $e'),
...transformersErrors.map((String e) => 'In $_transformersKey section of asset "$path": $e'),
];
if (errors.isNotEmpty) {
return (
null,
<String>[
'Unable to parse assets section.',
...errors
].join('\n'),
);
}
if (flavors is! YamlList) {
return(null, 'Asset manifest entry is malformed. '
'Expected "$_flavorKey" entry to be a list of strings. '
'Got ${flavors.runtimeType} instead.');
}
final List<String> flavorsListErrors = _validateList<String>(
flavors,
'flavors list of entry "$path"',
'String',
return (
AssetsEntry(
uri: Uri(pathSegments: path.split('/')),
flavors: Set<String>.from(flavors ?? <String>[]),
transformers: transformers ?? <AssetTransformerEntry>[],
),
null,
);
if (flavorsListErrors.isNotEmpty) {
return (null, 'Asset manifest entry is malformed. '
'Expected "$_flavorKey" entry to be a list of strings.\n'
'${flavorsListErrors.join('\n')}');
}
final AssetsEntry entry = AssetsEntry(
uri: Uri(pathSegments: path.split('/')),
flavors: Set<String>.from(flavors),
);
return (entry, null);
}
return (null, 'Assets entry had unexpected shape. '
'Expected a string or an object. Got ${yaml.runtimeType} instead.');
}
static (List<String>? flavors, List<String> errors) _parseFlavorsSection(Object? yaml) {
if (yaml == null) {
return (null, <String>[]);
}
return _parseList<String>(yaml, _flavorKey, 'String');
}
static (List<AssetTransformerEntry>?, List<String> errors) _parseTransformersSection(Object? yaml) {
if (yaml == null) {
return (null, <String>[]);
}
final (List<YamlMap>? yamlObjects, List<String> listErrors) = _parseList<YamlMap>(
yaml,
'$_transformersKey list',
'Map',
);
if (listErrors.isNotEmpty) {
return (null, listErrors);
}
final List<AssetTransformerEntry> transformers = <AssetTransformerEntry>[];
final List<String> errors = <String>[];
for (final YamlMap yaml in yamlObjects!) {
final (AssetTransformerEntry? transformerEntry, List<String> transformerErrors) = AssetTransformerEntry.tryParse(yaml);
if (transformerEntry != null) {
transformers.add(transformerEntry);
} else {
errors.addAll(transformerErrors);
}
}
if (errors.isEmpty) {
return (transformers, errors);
}
return (null, errors);
}
@override
bool operator ==(Object other) {
if (other is! AssetsEntry) {
@ -799,3 +840,91 @@ class AssetsEntry {
@override
String toString() => 'AssetsEntry(uri: $uri, flavors: $flavors)';
}
/// Represents an entry in the "transformers" section of an asset.
@immutable
final class AssetTransformerEntry {
const AssetTransformerEntry({
required this.package,
required List<String>? args,
}): args = args ?? const <String>[];
final String package;
final List<String>? args;
static (AssetTransformerEntry? entry, List<String> errors) tryParse(Object? yaml) {
if (yaml == null) {
return (null, <String>['Transformer entry is null.']);
}
if (yaml is! YamlMap) {
return (null, <String>['Expected entry to be a map. Found ${yaml.runtimeType} instead']);
}
final Object? package = yaml['package'];
if (package is! String || package.isEmpty) {
return (null, <String>['Expected "package" to be a String. Found ${package.runtimeType} instead.']);
}
final (List<String>? args, List<String> argsErrors) = _parseArgsSection(yaml['args']);
if (argsErrors.isNotEmpty) {
return (null, argsErrors.map((String e) => 'In args section of transformer using package "$package": $e').toList());
}
return (
AssetTransformerEntry(
package: package,
args: args,
),
<String>[],
);
}
static (List<String>? args, List<String> errors) _parseArgsSection(Object? yaml) {
if (yaml == null) {
return (null, <String>[]);
}
return _parseList(yaml, 'args', 'String');
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other is! AssetTransformerEntry) {
return false;
}
final bool argsAreEqual = (() {
if (args == null && other.args == null) {
return true;
}
if (args?.length != other.args?.length) {
return false;
}
for (int index = 0; index < args!.length; index += 1) {
if (args![index] != other.args![index]) {
return false;
}
}
return true;
})();
return package == other.package && argsAreEqual;
}
@override
int get hashCode => Object.hashAll(
<Object?>[
package.hashCode,
args?.map((String e) => e.hashCode),
],
);
@override
String toString() {
return 'AssetTransformerEntry(package: $package, args: $args)';
}
}

View file

@ -154,8 +154,9 @@ flutter:
''';
FlutterManifest.createFromString(manifest, logger: logger);
expect(logger.errorText, contains(
'Asset manifest entry is malformed. '
'Expected "flavors" entry to be a list of strings.',
'Unable to parse assets section.\n'
'In flavors section of asset "assets/vanilla/": Expected flavors '
'to be a list of String, but element at index 0 was a YamlMap.\n'
));
});
});

View file

@ -0,0 +1,166 @@
// Copyright 2014 The Flutter Authors. 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:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/flutter_manifest.dart';
import '../src/common.dart';
void main() {
group('parsing of assets section in flutter manifests with asset transformers', () {
testWithoutContext('parses an asset with a simple transformation', () async {
final BufferLogger logger = BufferLogger.test();
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- path: asset/hello.txt
transformers:
- package: my_package
''';
final FlutterManifest? parsedManifest = FlutterManifest.createFromString(manifest, logger: logger);
expect(parsedManifest!.assets, <AssetsEntry>[
AssetsEntry(
uri: Uri.parse('asset/hello.txt'),
transformers: const <AssetTransformerEntry>[
AssetTransformerEntry(package: 'my_package', args: <String>[])
],
),
]);
expect(logger.errorText, isEmpty);
});
testWithoutContext('parses an asset with a transformation that has args', () async {
final BufferLogger logger = BufferLogger.test();
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- path: asset/hello.txt
transformers:
- package: my_package
args: ["-e", "--color", "purple"]
''';
final FlutterManifest? parsedManifest = FlutterManifest.createFromString(manifest, logger: logger);
expect(parsedManifest!.assets, <AssetsEntry>[
AssetsEntry(
uri: Uri.parse('asset/hello.txt'),
transformers: const <AssetTransformerEntry>[
AssetTransformerEntry(
package: 'my_package',
args: <String>['-e', '--color', 'purple'],
)
],
),
]);
expect(logger.errorText, isEmpty);
});
testWithoutContext('fails when a transformers section is not a list', () async {
final BufferLogger logger = BufferLogger.test();
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- path: asset/hello.txt
transformers:
- my_transformer
''';
FlutterManifest.createFromString(manifest, logger: logger);
expect(
logger.errorText,
'Unable to parse assets section.\n'
'In transformers section of asset "asset/hello.txt": Expected '
'transformers list to be a list of Map, but element at index 0 was a String.\n',
);
});
testWithoutContext('fails when a transformers section package is not a string', () async {
final BufferLogger logger = BufferLogger.test();
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- path: asset/hello.txt
transformers:
- package:
i am a key: i am a value
''';
FlutterManifest.createFromString(manifest, logger: logger);
expect(
logger.errorText,
'Unable to parse assets section.\n'
'In transformers section of asset "asset/hello.txt": '
'Expected "package" to be a String. Found YamlMap instead.\n',
);
});
testWithoutContext('fails when a transformer is missing the package field', () async {
final BufferLogger logger = BufferLogger.test();
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- path: asset/hello.txt
transformers:
- args: ["-e"]
''';
FlutterManifest.createFromString(manifest, logger: logger);
expect(
logger.errorText,
'Unable to parse assets section.\n'
'In transformers section of asset "asset/hello.txt": Expected "package" to be a '
'String. Found Null instead.\n',
);
});
testWithoutContext('fails when a transformer has args field that is not a list of strings', () async {
final BufferLogger logger = BufferLogger.test();
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- path: asset/hello.txt
transformers:
- package: my_transformer
args: hello
''';
FlutterManifest.createFromString(manifest, logger: logger);
expect(
logger.errorText,
'Unable to parse assets section.\n'
'In transformers section of asset "asset/hello.txt": In args section '
'of transformer using package "my_transformer": Expected args to be a '
'list of String, but got hello (String).\n',
);
});
});
}