diff --git a/packages/flutter_tools/lib/src/flutter_manifest.dart b/packages/flutter_tools/lib/src/flutter_manifest.dart index 5ce2f72661..d28b38349c 100644 --- a/packages/flutter_tools/lib/src/flutter_manifest.dart +++ b/packages/flutter_tools/lib/src/flutter_manifest.dart @@ -519,7 +519,8 @@ void _validateFlutter(YamlMap? yaml, List errors) { _validateFonts(yamlValue, errors); } case 'licenses': - errors.addAll(_validateList(yamlValue, '"$yamlKey"', 'files')); + final (_, List filesErrors) = _parseList(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 errors) { } } -List _validateList(Object? yamlList, String context, String typeAlias) { +(List? result, List errors) _parseList(Object? yamlList, String context, String typeAlias) { final List errors = []; if (yamlList is! YamlList) { - return ['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, [message]); } for (int i = 0; i < yamlList.length; i++) { @@ -567,8 +569,9 @@ List _validateList(Object? yamlList, String context, String typeAlias } } - return errors; + return errors.isEmpty ? (List.from(yamlList), errors) : (null, errors); } + void _validateDeferredComponents(MapEntry kvp, List errors) { final Object? yamlList = kvp.value; if (yamlList != null && (yamlList is! YamlList || yamlList[0] is! YamlMap)) { @@ -585,11 +588,12 @@ void _validateDeferredComponents(MapEntry kvp, List 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( + final (_, List librariesErrors) = _parseList( 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 {}, + this.transformers = const [], }); final Uri uri; final Set flavors; + final List 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? flavors, List flavorsErrors) = _parseFlavorsSection(yaml[_flavorKey]); + final (List? transformers, List transformersErrors) = _parseTransformersSection(yaml[_transformersKey]); - if (flavors == null) { - return (AssetsEntry(uri: uri), null); + final List errors = [ + ...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, + [ + '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 flavorsListErrors = _validateList( - flavors, - 'flavors list of entry "$path"', - 'String', + return ( + AssetsEntry( + uri: Uri(pathSegments: path.split('/')), + flavors: Set.from(flavors ?? []), + transformers: transformers ?? [], + ), + 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.from(flavors), - ); - - return (entry, null); } return (null, 'Assets entry had unexpected shape. ' 'Expected a string or an object. Got ${yaml.runtimeType} instead.'); } + static (List? flavors, List errors) _parseFlavorsSection(Object? yaml) { + if (yaml == null) { + return (null, []); + } + + return _parseList(yaml, _flavorKey, 'String'); + } + + static (List?, List errors) _parseTransformersSection(Object? yaml) { + if (yaml == null) { + return (null, []); + } + final (List? yamlObjects, List listErrors) = _parseList( + yaml, + '$_transformersKey list', + 'Map', + ); + + if (listErrors.isNotEmpty) { + return (null, listErrors); + } + + final List transformers = []; + final List errors = []; + for (final YamlMap yaml in yamlObjects!) { + final (AssetTransformerEntry? transformerEntry, List 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? args, + }): args = args ?? const []; + + final String package; + final List? args; + + static (AssetTransformerEntry? entry, List errors) tryParse(Object? yaml) { + if (yaml == null) { + return (null, ['Transformer entry is null.']); + } + if (yaml is! YamlMap) { + return (null, ['Expected entry to be a map. Found ${yaml.runtimeType} instead']); + } + + final Object? package = yaml['package']; + if (package is! String || package.isEmpty) { + return (null, ['Expected "package" to be a String. Found ${package.runtimeType} instead.']); + } + + final (List? args, List 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, + ), + [], + ); + } + + static (List? args, List errors) _parseArgsSection(Object? yaml) { + if (yaml == null) { + return (null, []); + } + 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( + [ + package.hashCode, + args?.map((String e) => e.hashCode), + ], + ); + + @override + String toString() { + return 'AssetTransformerEntry(package: $package, args: $args)'; + } +} diff --git a/packages/flutter_tools/test/general.shard/flutter_manifest_assets_test.dart b/packages/flutter_tools/test/general.shard/flutter_manifest_assets_test.dart index 3d76855fd8..b4557a4985 100644 --- a/packages/flutter_tools/test/general.shard/flutter_manifest_assets_test.dart +++ b/packages/flutter_tools/test/general.shard/flutter_manifest_assets_test.dart @@ -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' )); }); }); diff --git a/packages/flutter_tools/test/general.shard/flutter_manifest_assets_transformers_test.dart b/packages/flutter_tools/test/general.shard/flutter_manifest_assets_transformers_test.dart new file mode 100644 index 0000000000..228d0bbfc8 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/flutter_manifest_assets_transformers_test.dart @@ -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( + uri: Uri.parse('asset/hello.txt'), + transformers: const [ + AssetTransformerEntry(package: 'my_package', args: []) + ], + ), + ]); + + 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( + uri: Uri.parse('asset/hello.txt'), + transformers: const [ + AssetTransformerEntry( + package: 'my_package', + args: ['-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', + ); + }); + }); +}