Unverified Commit 3a18473b authored by Andrew Kolos's avatar Andrew Kolos Committed by GitHub

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.
parent 848aa508
...@@ -519,7 +519,8 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) { ...@@ -519,7 +519,8 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) {
_validateFonts(yamlValue, errors); _validateFonts(yamlValue, errors);
} }
case 'licenses': case 'licenses':
errors.addAll(_validateList<String>(yamlValue, '"$yamlKey"', 'files')); final (_, List<String> filesErrors) = _parseList<String>(yamlValue, '"$yamlKey"', 'files');
errors.addAll(filesErrors);
case 'module': case 'module':
if (yamlValue is! YamlMap) { if (yamlValue is! YamlMap) {
errors.add('Expected "$yamlKey" to be an object, but got $yamlValue (${yamlValue.runtimeType}).'); errors.add('Expected "$yamlKey" to be an object, but got $yamlValue (${yamlValue.runtimeType}).');
...@@ -553,11 +554,12 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) { ...@@ -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>[]; final List<String> errors = <String>[];
if (yamlList is! YamlList) { 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++) { for (int i = 0; i < yamlList.length; i++) {
...@@ -567,8 +569,9 @@ List<String> _validateList<T>(Object? yamlList, String context, String typeAlias ...@@ -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) { void _validateDeferredComponents(MapEntry<Object?, Object?> kvp, List<String> errors) {
final Object? yamlList = kvp.value; final Object? yamlList = kvp.value;
if (yamlList != null && (yamlList is! YamlList || yamlList[0] is! YamlMap)) { if (yamlList != null && (yamlList is! YamlList || yamlList[0] is! YamlMap)) {
...@@ -585,11 +588,12 @@ void _validateDeferredComponents(MapEntry<Object?, Object?> kvp, List<String> er ...@@ -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'); errors.add('Expected the $i element in "${kvp.key}" to have required key "name" of type String');
} }
if (valueMap.containsKey('libraries')) { if (valueMap.containsKey('libraries')) {
errors.addAll(_validateList<String>( final (_, List<String> librariesErrors) = _parseList<String>(
valueMap['libraries'], valueMap['libraries'],
'"libraries" key in the element at index $i of "${kvp.key}"', '"libraries" key in the element at index $i of "${kvp.key}"',
'String', 'String',
)); );
errors.addAll(librariesErrors);
} }
if (valueMap.containsKey('assets')) { if (valueMap.containsKey('assets')) {
errors.addAll(_validateAssets(valueMap['assets'])); errors.addAll(_validateAssets(valueMap['assets']));
...@@ -697,13 +701,16 @@ class AssetsEntry { ...@@ -697,13 +701,16 @@ class AssetsEntry {
const AssetsEntry({ const AssetsEntry({
required this.uri, required this.uri,
this.flavors = const <String>{}, this.flavors = const <String>{},
this.transformers = const <AssetTransformerEntry>[],
}); });
final Uri uri; final Uri uri;
final Set<String> flavors; final Set<String> flavors;
final List<AssetTransformerEntry> transformers;
static const String _pathKey = 'path'; static const String _pathKey = 'path';
static const String _flavorKey = 'flavors'; static const String _flavorKey = 'flavors';
static const String _transformersKey = 'transformers';
static AssetsEntry? parseFromYaml(Object? yaml) { static AssetsEntry? parseFromYaml(Object? yaml) {
final (AssetsEntry? value, String? error) = parseFromYamlSafe(yaml); final (AssetsEntry? value, String? error) = parseFromYamlSafe(yaml);
...@@ -738,7 +745,6 @@ class AssetsEntry { ...@@ -738,7 +745,6 @@ class AssetsEntry {
} }
final Object? path = yaml[_pathKey]; final Object? path = yaml[_pathKey];
final Object? flavors = yaml[_flavorKey];
if (path == null || path is! String) { if (path == null || path is! String) {
return (null, 'Asset manifest entry is malformed. ' return (null, 'Asset manifest entry is malformed. '
...@@ -746,41 +752,76 @@ class AssetsEntry { ...@@ -746,41 +752,76 @@ class AssetsEntry {
'containing a "$_pathKey" entry. Got ${path.runtimeType} instead.'); '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 (flavors is! YamlList) { ];
return(null, 'Asset manifest entry is malformed. ' if (errors.isNotEmpty) {
'Expected "$_flavorKey" entry to be a list of strings. ' return (
'Got ${flavors.runtimeType} instead.'); null,
} <String>[
'Unable to parse assets section.',
final List<String> flavorsListErrors = _validateList<String>( ...errors
flavors, ].join('\n'),
'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 AssetsEntry entry = AssetsEntry( return (
uri: Uri(pathSegments: path.split('/')), AssetsEntry(
flavors: Set<String>.from(flavors), 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. ' return (null, 'Assets entry had unexpected shape. '
'Expected a string or an object. Got ${yaml.runtimeType} instead.'); '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 @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! AssetsEntry) { if (other is! AssetsEntry) {
...@@ -799,3 +840,91 @@ class AssetsEntry { ...@@ -799,3 +840,91 @@ class AssetsEntry {
@override @override
String toString() => 'AssetsEntry(uri: $uri, flavors: $flavors)'; 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)';
}
}
...@@ -154,8 +154,9 @@ flutter: ...@@ -154,8 +154,9 @@ flutter:
'''; ''';
FlutterManifest.createFromString(manifest, logger: logger); FlutterManifest.createFromString(manifest, logger: logger);
expect(logger.errorText, contains( expect(logger.errorText, contains(
'Asset manifest entry is malformed. ' 'Unable to parse assets section.\n'
'Expected "flavors" entry to be a list of strings.', 'In flavors section of asset "assets/vanilla/": Expected flavors '
'to be a list of String, but element at index 0 was a YamlMap.\n'
)); ));
}); });
}); });
......
// 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',
);
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment