Skip to content

Commit

Permalink
add parsing of assets transformer declarations in pubspec.yaml (#143557)
Browse files Browse the repository at this point in the history
In service of flutter/flutter#143348.

This PR enables parsing of the pubspec yaml schemes for assets with transformations as described in #143348.
  • Loading branch information
andrewkolos authored Feb 16, 2024
1 parent 848aa50 commit 3a18473
Show file tree
Hide file tree
Showing 3 changed files with 331 additions and 35 deletions.
195 changes: 162 additions & 33 deletions packages/flutter_tools/lib/src/flutter_manifest.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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}).');
Expand Down Expand Up @@ -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++) {
Expand All @@ -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)) {
Expand All @@ -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']));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -738,49 +745,83 @@ 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. '
'Expected asset entry to be either a string or a map '
'containing a "$_pathKey" entry. Got ${path.runtimeType} instead.');
}

final Uri uri = Uri(pathSegments: path.split('/'));

if (flavors == null) {
return (AssetsEntry(uri: uri), null);
}

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',
);
if (flavorsListErrors.isNotEmpty) {
return (null, 'Asset manifest entry is malformed. '
'Expected "$_flavorKey" entry to be a list of strings.\n'
'${flavorsListErrors.join('\n')}');
final (List<String>? flavors, List<String> flavorsErrors) = _parseFlavorsSection(yaml[_flavorKey]);
final (List<AssetTransformerEntry>? transformers, List<String> transformersErrors) = _parseTransformersSection(yaml[_transformersKey]);

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'),
);
}

final AssetsEntry entry = AssetsEntry(
uri: Uri(pathSegments: path.split('/')),
flavors: Set<String>.from(flavors),
return (
AssetsEntry(
uri: Uri(pathSegments: path.split('/')),
flavors: Set<String>.from(flavors ?? <String>[]),
transformers: transformers ?? <AssetTransformerEntry>[],
),
null,
);

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) {
Expand All @@ -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)';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'
));
});
});
Expand Down
Loading

0 comments on commit 3a18473

Please sign in to comment.