Unverified Commit 14bcc694 authored by Andrew Kolos's avatar Andrew Kolos Committed by GitHub

Fix `AssetsEntry::equals` (#143355)

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

**Issue.** The `equals` implementation of `AssetsEntry` is incorrect. It compares `flavors` lists using reference equality. This PR addresses this.

This also adds a test to make sure valid asset `flavors` declarations are parsed correctly.

While we are here, this PR also includes a couple of refactorings:
  * `flutter_manifest_test.dart` is a bit large. To better match our style guide, I've factored out some related tests into their own file.
  *  A couple of changes to the `_validateListType` function in `flutter_manifest.dart`:
      * The function now returns a list of errors instead of accepting a list to append onto. This is more readable and also allows callers to know which errors were found by the call.
      * The function is renamed to `_validateList` and now accepts an `Object?` instead of an `YamlList`. If the argument is null, an appropriate error message is contained in the output. This saves callers that are only interested in validation from having to write their own null-check, which they all did before.
      * Some error strings were tweaked for increased readability and/or grammatical correctness.
parent 4280d295
...@@ -949,7 +949,7 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -949,7 +949,7 @@ class ManifestAssetBundle implements AssetBundle {
Uri assetUri, { Uri assetUri, {
String? packageName, String? packageName,
Package? attributedPackage, Package? attributedPackage,
List<String>? flavors, Set<String>? flavors,
}) { }) {
final String directoryPath; final String directoryPath;
try { try {
...@@ -1000,7 +1000,7 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -1000,7 +1000,7 @@ class ManifestAssetBundle implements AssetBundle {
String? packageName, String? packageName,
Package? attributedPackage, Package? attributedPackage,
AssetKind assetKind = AssetKind.regular, AssetKind assetKind = AssetKind.regular,
List<String>? flavors, Set<String>? flavors,
}) { }) {
final _Asset asset = _resolveAsset( final _Asset asset = _resolveAsset(
packageConfig, packageConfig,
...@@ -1116,7 +1116,7 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -1116,7 +1116,7 @@ class ManifestAssetBundle implements AssetBundle {
Package? attributedPackage, { Package? attributedPackage, {
Uri? originUri, Uri? originUri,
AssetKind assetKind = AssetKind.regular, AssetKind assetKind = AssetKind.regular,
List<String>? flavors, Set<String>? flavors,
}) { }) {
final String assetPath = _fileSystem.path.fromUri(assetUri); final String assetPath = _fileSystem.path.fromUri(assetUri);
if (assetUri.pathSegments.first == 'packages' if (assetUri.pathSegments.first == 'packages'
...@@ -1155,7 +1155,7 @@ class ManifestAssetBundle implements AssetBundle { ...@@ -1155,7 +1155,7 @@ class ManifestAssetBundle implements AssetBundle {
Package? attributedPackage, { Package? attributedPackage, {
AssetKind assetKind = AssetKind.regular, AssetKind assetKind = AssetKind.regular,
Uri? originUri, Uri? originUri,
List<String>? flavors, Set<String>? flavors,
}) { }) {
assert(assetUri.pathSegments.first == 'packages'); assert(assetUri.pathSegments.first == 'packages');
if (assetUri.pathSegments.length > 1) { if (assetUri.pathSegments.length > 1) {
...@@ -1192,8 +1192,8 @@ class _Asset { ...@@ -1192,8 +1192,8 @@ class _Asset {
required this.entryUri, required this.entryUri,
required this.package, required this.package,
this.kind = AssetKind.regular, this.kind = AssetKind.regular,
List<String>? flavors, Set<String>? flavors,
}): originUri = originUri ?? entryUri, flavors = flavors ?? const <String>[]; }): originUri = originUri ?? entryUri, flavors = flavors ?? const <String>{};
final String baseDir; final String baseDir;
...@@ -1212,7 +1212,7 @@ class _Asset { ...@@ -1212,7 +1212,7 @@ class _Asset {
final AssetKind kind; final AssetKind kind;
final List<String> flavors; final Set<String> flavors;
File lookupAssetFile(FileSystem fileSystem) { File lookupAssetFile(FileSystem fileSystem) {
return fileSystem.file(fileSystem.path.join(baseDir, fileSystem.path.fromUri(relativeUri))); return fileSystem.file(fileSystem.path.join(baseDir, fileSystem.path.fromUri(relativeUri)));
......
...@@ -479,3 +479,22 @@ Match? firstMatchInFile(File file, RegExp regExp) { ...@@ -479,3 +479,22 @@ Match? firstMatchInFile(File file, RegExp regExp) {
} }
return null; return null;
} }
/// Tests for shallow equality on two sets.
bool setEquals<T>(Set<T>? a, Set<T>? b) {
if (a == null) {
return b == null;
}
if (b == null || a.length != b.length) {
return false;
}
if (identical(a, b)) {
return true;
}
for (final T value in a) {
if (!b.contains(value)) {
return false;
}
}
return true;
}
...@@ -519,17 +519,7 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) { ...@@ -519,17 +519,7 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) {
_validateFonts(yamlValue, errors); _validateFonts(yamlValue, errors);
} }
case 'licenses': case 'licenses':
if (yamlValue is! YamlList) { errors.addAll(_validateList<String>(yamlValue, '"$yamlKey"', 'files'));
errors.add('Expected "$yamlKey" to be a list of files, but got $yamlValue (${yamlValue.runtimeType})');
} else if (yamlValue.isEmpty) {
break;
} else if (yamlValue.first is! String) {
errors.add(
'Expected "$yamlKey" to contain strings, but the first element is $yamlValue (${yamlValue.runtimeType}).',
);
} else {
_validateListType<String>(yamlValue, errors, '"$yamlKey"', 'files');
}
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}).');
...@@ -563,15 +553,22 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) { ...@@ -563,15 +553,22 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) {
} }
} }
void _validateListType<T>(YamlList yamlList, List<String> errors, String context, String typeAlias) { List<String> _validateList<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}).'];
}
for (int i = 0; i < yamlList.length; i++) { for (int i = 0; i < yamlList.length; i++) {
if (yamlList[i] is! T) { if (yamlList[i] is! T) {
// ignore: avoid_dynamic_calls // ignore: avoid_dynamic_calls
errors.add('Expected $context to be a list of $typeAlias, but element $i was a ${yamlList[i].runtimeType}'); errors.add('Expected $context to be a list of $typeAlias, but element at index $i was a ${yamlList[i].runtimeType}.');
} }
} }
}
return 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)) {
...@@ -588,12 +585,11 @@ void _validateDeferredComponents(MapEntry<Object?, Object?> kvp, List<String> er ...@@ -588,12 +585,11 @@ 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')) {
final Object? libraries = valueMap['libraries']; errors.addAll(_validateList<String>(
if (libraries is! YamlList) { valueMap['libraries'],
errors.add('Expected "libraries" key in the $i element of "${kvp.key}" to be a list, but got $libraries (${libraries.runtimeType}).'); '"libraries" key in the element at index $i of "${kvp.key}"',
} else { 'String',
_validateListType<String>(libraries, errors, '"libraries" key in the $i element of "${kvp.key}"', 'dart library Strings'); ));
}
} }
if (valueMap.containsKey('assets')) { if (valueMap.containsKey('assets')) {
errors.addAll(_validateAssets(valueMap['assets'])); errors.addAll(_validateAssets(valueMap['assets']));
...@@ -700,11 +696,11 @@ void _validateFonts(YamlList fonts, List<String> errors) { ...@@ -700,11 +696,11 @@ void _validateFonts(YamlList fonts, List<String> errors) {
class AssetsEntry { class AssetsEntry {
const AssetsEntry({ const AssetsEntry({
required this.uri, required this.uri,
this.flavors = const <String>[], this.flavors = const <String>{},
}); });
final Uri uri; final Uri uri;
final List<String> flavors; final Set<String> flavors;
static const String _pathKey = 'path'; static const String _pathKey = 'path';
static const String _flavorKey = 'flavors'; static const String _flavorKey = 'flavors';
...@@ -762,8 +758,11 @@ class AssetsEntry { ...@@ -762,8 +758,11 @@ class AssetsEntry {
'Got ${flavors.runtimeType} instead.'); 'Got ${flavors.runtimeType} instead.');
} }
final List<String> flavorsListErrors = <String>[]; final List<String> flavorsListErrors = _validateList<String>(
_validateListType<String>(flavors, flavorsListErrors, 'flavors list of entry "$path"', 'String'); flavors,
'flavors list of entry "$path"',
'String',
);
if (flavorsListErrors.isNotEmpty) { if (flavorsListErrors.isNotEmpty) {
return (null, 'Asset manifest entry is malformed. ' return (null, 'Asset manifest entry is malformed. '
'Expected "$_flavorKey" entry to be a list of strings.\n' 'Expected "$_flavorKey" entry to be a list of strings.\n'
...@@ -772,7 +771,7 @@ class AssetsEntry { ...@@ -772,7 +771,7 @@ class AssetsEntry {
final AssetsEntry entry = AssetsEntry( final AssetsEntry entry = AssetsEntry(
uri: Uri(pathSegments: path.split('/')), uri: Uri(pathSegments: path.split('/')),
flavors: List<String>.from(flavors), flavors: Set<String>.from(flavors),
); );
return (entry, null); return (entry, null);
...@@ -788,9 +787,15 @@ class AssetsEntry { ...@@ -788,9 +787,15 @@ class AssetsEntry {
return false; return false;
} }
return uri == other.uri && flavors == other.flavors; return uri == other.uri && setEquals(flavors, other.flavors);
} }
@override @override
int get hashCode => Object.hash(uri.hashCode, flavors.hashCode); int get hashCode => Object.hashAll(<Object?>[
uri.hashCode,
Object.hashAllUnordered(flavors),
]);
@override
String toString() => 'AssetsEntry(uri: $uri, flavors: $flavors)';
} }
// 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', () {
testWithoutContext('ignores empty list of assets', () {
final BufferLogger logger = BufferLogger.test();
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
assets: []
''';
final FlutterManifest? flutterManifest = FlutterManifest.createFromString(
manifest,
logger: logger,
);
expect(flutterManifest, isNotNull);
expect(flutterManifest!.assets, isEmpty);
});
testWithoutContext('parses two simple asset declarations', () async {
final BufferLogger logger = BufferLogger.test();
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- a/foo
- a/bar
''';
final FlutterManifest flutterManifest = FlutterManifest.createFromString(
manifest,
logger: logger,
)!;
expect(flutterManifest.assets, <AssetsEntry>[
AssetsEntry(uri: Uri.parse('a/foo')),
AssetsEntry(uri: Uri.parse('a/bar')),
]);
});
testWithoutContext('does not crash on empty entry', () {
final BufferLogger logger = BufferLogger.test();
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- lib/gallery/example_code.dart
-
''';
FlutterManifest.createFromString(
manifest,
logger: logger,
);
expect(logger.errorText, contains('Asset manifest contains a null or empty uri.'));
});
testWithoutContext('handles special characters in asset URIs', () {
final BufferLogger logger = BufferLogger.test();
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- lib/gallery/abc#xyz
- lib/gallery/abc?xyz
- lib/gallery/aaa bbb
''';
final FlutterManifest flutterManifest = FlutterManifest.createFromString(
manifest,
logger: logger,
)!;
final List<AssetsEntry> assets = flutterManifest.assets;
expect(assets, <AssetsEntry>[
AssetsEntry(uri: Uri.parse('lib/gallery/abc%23xyz')),
AssetsEntry(uri: Uri.parse('lib/gallery/abc%3Fxyz')),
AssetsEntry(uri: Uri.parse('lib/gallery/aaa%20bbb')),
]);
});
testWithoutContext('parses an asset with flavors', () async {
final BufferLogger logger = BufferLogger.test();
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- path: a/foo
flavors:
- apple
- strawberry
''';
final FlutterManifest flutterManifest = FlutterManifest.createFromString(
manifest,
logger: logger,
)!;
expect(flutterManifest.assets, <AssetsEntry>[
AssetsEntry(
uri: Uri.parse('a/foo'),
flavors: const <String>{'apple', 'strawberry'},
),
]);
});
testWithoutContext("prints an error when an asset entry's flavor 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:
- assets/folder/
- path: assets/vanilla/
flavors:
- key1: value1
key2: value2
''';
FlutterManifest.createFromString(manifest, logger: logger);
expect(logger.errorText, contains(
'Asset manifest entry is malformed. '
'Expected "flavors" entry to be a list of strings.',
));
});
});
}
...@@ -136,50 +136,6 @@ flutter: ...@@ -136,50 +136,6 @@ flutter:
expect(flutterManifest.generateSyntheticPackage, false); expect(flutterManifest.generateSyntheticPackage, false);
}); });
testWithoutContext('FlutterManifest has two assets', () async {
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- a/foo
- a/bar
''';
final FlutterManifest flutterManifest = FlutterManifest.createFromString(
manifest,
logger: logger,
)!;
expect(flutterManifest.assets, <AssetsEntry>[
AssetsEntry(uri: Uri.parse('a/foo')),
AssetsEntry(uri: Uri.parse('a/bar')),
]);
});
testWithoutContext('FlutterManifest assets entry flavor is not a string', () async {
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- assets/folder/
- path: assets/vanilla/
flavors:
- key1: value1
key2: value2
''';
FlutterManifest.createFromString(manifest, logger: logger);
expect(logger.errorText, contains('Asset manifest entry is malformed. '
'Expected "flavors" entry to be a list of strings.'));
});
testWithoutContext('FlutterManifest has one font family with one asset', () async { testWithoutContext('FlutterManifest has one font family with one asset', () async {
const String manifest = ''' const String manifest = '''
name: test name: test
...@@ -796,25 +752,6 @@ flutter: ...@@ -796,25 +752,6 @@ flutter:
expect(flutterManifest!.fonts.length, 0); expect(flutterManifest!.fonts.length, 0);
}); });
testWithoutContext('FlutterManifest ignores empty list of assets', () {
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
assets: []
''';
final FlutterManifest? flutterManifest = FlutterManifest.createFromString(
manifest,
logger: logger,
);
expect(flutterManifest, isNotNull);
expect(flutterManifest!.assets.length, 0);
});
testWithoutContext('FlutterManifest returns proper error when font detail is ' testWithoutContext('FlutterManifest returns proper error when font detail is '
'not a list of maps', () { 'not a list of maps', () {
const String manifest = ''' const String manifest = '''
...@@ -887,55 +824,6 @@ flutter: ...@@ -887,55 +824,6 @@ flutter:
expect(logger.errorText, contains('Expected a map.')); expect(logger.errorText, contains('Expected a map.'));
}); });
testWithoutContext('FlutterManifest does not crash on empty entry', () {
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- lib/gallery/example_code.dart
-
''';
FlutterManifest.createFromString(
manifest,
logger: logger,
);
expect(logger.errorText, contains('Asset manifest contains a null or empty uri.'));
});
testWithoutContext('FlutterManifest handles special characters in asset URIs', () {
const String manifest = '''
name: test
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- lib/gallery/abc#xyz
- lib/gallery/abc?xyz
- lib/gallery/aaa bbb
''';
final FlutterManifest flutterManifest = FlutterManifest.createFromString(
manifest,
logger: logger,
)!;
final List<AssetsEntry> assets = flutterManifest.assets;
expect(assets, hasLength(3));
expect(assets, <AssetsEntry>[
AssetsEntry(uri: Uri.parse('lib/gallery/abc%23xyz')),
AssetsEntry(uri: Uri.parse('lib/gallery/abc%3Fxyz')),
AssetsEntry(uri: Uri.parse('lib/gallery/aaa%20bbb')),
]);
});
testWithoutContext('FlutterManifest returns proper error when flutter is a ' testWithoutContext('FlutterManifest returns proper error when flutter is a '
'list instead of a map', () { 'list instead of a map', () {
const String manifest = ''' const String manifest = '''
...@@ -1186,7 +1074,7 @@ flutter: ...@@ -1186,7 +1074,7 @@ flutter:
); );
expect(flutterManifest, null); expect(flutterManifest, null);
expect(logger.errorText, 'Expected "licenses" to be a list of files, but got foo.txt (String)\n'); expect(logger.errorText, 'Expected "licenses" to be a list of files, but got foo.txt (String).\n');
}); });
testWithoutContext('FlutterManifest validates individual list items', () async { testWithoutContext('FlutterManifest validates individual list items', () async {
...@@ -1207,7 +1095,8 @@ flutter: ...@@ -1207,7 +1095,8 @@ flutter:
); );
expect(flutterManifest, null); expect(flutterManifest, null);
expect(logger.errorText, 'Expected "licenses" to be a list of files, but element 1 was a YamlMap\n'); expect(logger.errorText, 'Expected "licenses" to be a list of files, but '
'element at index 1 was a YamlMap.\n');
}); });
testWithoutContext('FlutterManifest parses single deferred components', () async { testWithoutContext('FlutterManifest parses single deferred components', () async {
...@@ -1360,7 +1249,9 @@ flutter: ...@@ -1360,7 +1249,9 @@ flutter:
); );
expect(flutterManifest, null); expect(flutterManifest, null);
expect(logger.errorText, 'Expected "libraries" key in the 0 element of "deferred-components" to be a list, but got blah (String).\n'); expect(logger.errorText, 'Expected "libraries" key in the element at '
'index 0 of "deferred-components" to be a list of String, but '
'got blah (String).\n');
}); });
testWithoutContext('FlutterManifest deferred component libraries is string', () async { testWithoutContext('FlutterManifest deferred component libraries is string', () async {
...@@ -1382,7 +1273,9 @@ flutter: ...@@ -1382,7 +1273,9 @@ flutter:
); );
expect(flutterManifest, null); expect(flutterManifest, null);
expect(logger.errorText, 'Expected "libraries" key in the 0 element of "deferred-components" to be a list of dart library Strings, but element 0 was a YamlMap\n'); expect(logger.errorText, 'Expected "libraries" key in the element at '
'index 0 of "deferred-components" to be a list of String, but '
'element at index 0 was a YamlMap.\n');
}); });
testWithoutContext('FlutterManifest deferred component assets is string', () async { testWithoutContext('FlutterManifest deferred component assets is string', () async {
......
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