From adb68301eebfdd03f3aefb44020d6ff999ec8611 Mon Sep 17 00:00:00 2001 From: Dhruv Maradiya Date: Sun, 19 Oct 2025 17:08:04 +0530 Subject: [PATCH 1/5] Add a toJson method on Pubspec Closes #1801 - Added the serializable to serialize object into . - Added tests to validate functionality round-trip the shape of the source. - Updated to document the changes. --- pkgs/pubspec_parse/CHANGELOG.md | 3 +- pkgs/pubspec_parse/lib/src/dependency.dart | 33 +- pkgs/pubspec_parse/lib/src/pubspec.dart | 28 +- pkgs/pubspec_parse/lib/src/pubspec.g.dart | 25 ++ pkgs/pubspec_parse/lib/src/screenshot.dart | 8 + pkgs/pubspec_parse/pubspec.yaml | 2 +- pkgs/pubspec_parse/test/tojson_test.dart | 281 ++++++++++++++++++ .../test/directory_watcher/file_tests.dart | 97 +++--- .../test/directory_watcher/link_tests.dart | 6 - .../test/directory_watcher/linux_test.dart | 23 +- .../test/directory_watcher/mac_os_test.dart | 48 ++- .../test/directory_watcher/polling_test.dart | 4 +- .../test/directory_watcher/windows_test.dart | 2 +- .../watcher/test/file_watcher/file_tests.dart | 10 +- .../watcher/test/file_watcher/link_tests.dart | 6 - pkgs/watcher/test/utils.dart | 71 ++++- 16 files changed, 542 insertions(+), 105 deletions(-) create mode 100644 pkgs/pubspec_parse/test/tojson_test.dart diff --git a/pkgs/pubspec_parse/CHANGELOG.md b/pkgs/pubspec_parse/CHANGELOG.md index 31b30456b..26bf1035c 100644 --- a/pkgs/pubspec_parse/CHANGELOG.md +++ b/pkgs/pubspec_parse/CHANGELOG.md @@ -1,5 +1,6 @@ -## 1.5.1-wip +## 1.6.0-wip +- Added `toJson` method to `Pubspec` to serialize the object back to a `Map`. - Require Dart 3.8 ## 1.5.0 diff --git a/pkgs/pubspec_parse/lib/src/dependency.dart b/pkgs/pubspec_parse/lib/src/dependency.dart index ff052fa64..4658ac38e 100644 --- a/pkgs/pubspec_parse/lib/src/dependency.dart +++ b/pkgs/pubspec_parse/lib/src/dependency.dart @@ -94,7 +94,9 @@ Dependency? _fromJson(Object? data, String name) { return null; } -sealed class Dependency {} +sealed class Dependency { + Map toJson(); +} @JsonSerializable() class SdkDependency extends Dependency { @@ -114,6 +116,9 @@ class SdkDependency extends Dependency { @override String toString() => 'SdkDependency: $sdk'; + + @override + Map toJson() => {'sdk': sdk, 'version': version.toString()}; } @JsonSerializable() @@ -149,6 +154,15 @@ class GitDependency extends Dependency { @override String toString() => 'GitDependency: url@$url'; + + @override + Map toJson() => { + 'git': { + 'url': url.toString(), + if (ref != null) 'ref': ref, + if (path != null) 'path': path, + }, + }; } Uri? parseGitUriOrNull(String? value) => @@ -208,6 +222,9 @@ class PathDependency extends Dependency { @override String toString() => 'PathDependency: path@$path'; + + @override + Map toJson() => {'path': path}; } @JsonSerializable(disallowUnrecognizedKeys: true) @@ -232,6 +249,12 @@ class HostedDependency extends Dependency { @override String toString() => 'HostedDependency: $version'; + + @override + Map toJson() => { + 'version': version.toString(), + if (hosted != null) 'hosted': hosted!.toJson(), + }; } @JsonSerializable(disallowUnrecognizedKeys: true) @@ -275,7 +298,15 @@ class HostedDetails { @override int get hashCode => Object.hash(name, url); + + Map toJson() => { + if (declaredName != null) 'name': declaredName, + 'url': url.toString(), + }; } VersionConstraint _constraintFromString(String? input) => input == null ? VersionConstraint.any : VersionConstraint.parse(input); + +Map serializeDeps(Map input) => + input.map((k, v) => MapEntry(k, v.toJson())); diff --git a/pkgs/pubspec_parse/lib/src/pubspec.dart b/pkgs/pubspec_parse/lib/src/pubspec.dart index 01327f4a3..b8c2aea71 100644 --- a/pkgs/pubspec_parse/lib/src/pubspec.dart +++ b/pkgs/pubspec_parse/lib/src/pubspec.dart @@ -11,13 +11,13 @@ import 'screenshot.dart'; part 'pubspec.g.dart'; -@JsonSerializable() +@JsonSerializable(createToJson: true) class Pubspec { // TODO: executables final String name; - @JsonKey(fromJson: _versionFromString) + @JsonKey(fromJson: _versionFromString, toJson: _versionToString) final Version? version; final String? description; @@ -51,7 +51,7 @@ class Pubspec { final List? ignoredAdvisories; /// Optional field for specifying included screenshot files. - @JsonKey(fromJson: parseScreenshots) + @JsonKey(fromJson: parseScreenshots, toJson: serializeScreenshots) final List? screenshots; /// If there is exactly 1 value in [authors], returns it. @@ -69,16 +69,16 @@ class Pubspec { final List authors; final String? documentation; - @JsonKey(fromJson: _environmentMap) + @JsonKey(fromJson: _environmentMap, toJson: _serializeEnvironment) final Map environment; - @JsonKey(fromJson: parseDeps) + @JsonKey(fromJson: parseDeps, toJson: serializeDeps) final Map dependencies; - @JsonKey(fromJson: parseDeps) + @JsonKey(fromJson: parseDeps, toJson: serializeDeps) final Map devDependencies; - @JsonKey(fromJson: parseDeps) + @JsonKey(fromJson: parseDeps, toJson: serializeDeps) final Map dependencyOverrides; /// Optional configuration specific to [Flutter](https://flutter.io/) @@ -90,7 +90,7 @@ class Pubspec { final Map? flutter; /// Optional field to specify executables - @JsonKey(fromJson: _executablesMap) + @JsonKey(fromJson: _executablesMap, toJson: _serializeExecutables) final Map executables; /// If this package is a Pub Workspace, this field lists the sub-packages. @@ -171,6 +171,9 @@ class Pubspec { return _$PubspecFromJson(json); } + // Returns a JSON-serializable map for this instance. + Map toJson() => _$PubspecToJson(this); + /// Parses source [yaml] into [Pubspec]. /// /// When [lenient] is set, top-level property-parsing or type cast errors are @@ -247,3 +250,12 @@ Map _executablesMap(Map? source) => } }) ?? {}; + +Map _serializeEnvironment( + Map map, +) => map.map((key, value) => MapEntry(key, value?.toString())); + +String? _versionToString(Version? version) => version?.toString(); + +Map _serializeExecutables(Map? map) => + map?.map(MapEntry.new) ?? {}; diff --git a/pkgs/pubspec_parse/lib/src/pubspec.g.dart b/pkgs/pubspec_parse/lib/src/pubspec.g.dart index fa2222567..326f5f60a 100644 --- a/pkgs/pubspec_parse/lib/src/pubspec.g.dart +++ b/pkgs/pubspec_parse/lib/src/pubspec.g.dart @@ -92,3 +92,28 @@ Pubspec _$PubspecFromJson(Map json) => $checkedCreate( 'dependencyOverrides': 'dependency_overrides', }, ); + +Map _$PubspecToJson(Pubspec instance) => { + 'name': instance.name, + 'version': _versionToString(instance.version), + 'description': instance.description, + 'homepage': instance.homepage, + 'publish_to': instance.publishTo, + 'repository': instance.repository?.toString(), + 'issue_tracker': instance.issueTracker?.toString(), + 'funding': instance.funding?.map((e) => e.toString()).toList(), + 'topics': instance.topics, + 'ignored_advisories': instance.ignoredAdvisories, + 'screenshots': serializeScreenshots(instance.screenshots), + 'author': instance.author, + 'authors': instance.authors, + 'documentation': instance.documentation, + 'environment': _serializeEnvironment(instance.environment), + 'dependencies': serializeDeps(instance.dependencies), + 'dev_dependencies': serializeDeps(instance.devDependencies), + 'dependency_overrides': serializeDeps(instance.dependencyOverrides), + 'flutter': instance.flutter, + 'executables': _serializeExecutables(instance.executables), + 'workspace': instance.workspace, + 'resolution': instance.resolution, +}; diff --git a/pkgs/pubspec_parse/lib/src/screenshot.dart b/pkgs/pubspec_parse/lib/src/screenshot.dart index f5f0be2ea..1ca861367 100644 --- a/pkgs/pubspec_parse/lib/src/screenshot.dart +++ b/pkgs/pubspec_parse/lib/src/screenshot.dart @@ -63,3 +63,11 @@ List parseScreenshots(List? input) { } return res; } + +List> serializeScreenshots(List? input) => + input + ?.map( + (e) => {'description': e.description, 'path': e.path}, + ) + .toList() ?? + []; diff --git a/pkgs/pubspec_parse/pubspec.yaml b/pkgs/pubspec_parse/pubspec.yaml index 0769ddf55..699a01398 100644 --- a/pkgs/pubspec_parse/pubspec.yaml +++ b/pkgs/pubspec_parse/pubspec.yaml @@ -1,5 +1,5 @@ name: pubspec_parse -version: 1.5.1-wip +version: 1.6.0-wip description: >- Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting. diff --git a/pkgs/pubspec_parse/test/tojson_test.dart b/pkgs/pubspec_parse/test/tojson_test.dart new file mode 100644 index 000000000..1c23662e2 --- /dev/null +++ b/pkgs/pubspec_parse/test/tojson_test.dart @@ -0,0 +1,281 @@ +// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:test/test.dart'; + +import 'test_utils.dart'; + +void main() { + group('Pubspec toJson tests', () { + test('minimal set values', () async { + final value = await parse(defaultPubspec); + final jsonValue = value.toJson(); + + expect(jsonValue['name'], 'sample'); + expect(jsonValue['version'], isNull); + expect(jsonValue['publishTo'], isNull); + expect(jsonValue['description'], isNull); + expect(jsonValue['homepage'], isNull); + expect(jsonValue['author'], isNull); + expect(jsonValue['authors'], isEmpty); + expect(jsonValue['environment'], {'sdk': '>=2.12.0 <3.0.0'}); + expect(jsonValue['documentation'], isNull); + expect(jsonValue['dependencies'], isEmpty); + expect(jsonValue['dev_dependencies'], isEmpty); + expect(jsonValue['dependency_overrides'], isEmpty); + expect(jsonValue['flutter'], isNull); + expect(jsonValue['repository'], isNull); + expect(jsonValue['issue_tracker'], isNull); + expect(jsonValue['screenshots'], isEmpty); + expect(jsonValue['workspace'], isNull); + expect(jsonValue['resolution'], isNull); + expect(jsonValue['executables'], isEmpty); + }); + + test('all fields set', () async { + final version = Version.parse('1.2.3'); + final sdkConstraint = VersionConstraint.parse('>=3.6.0 <4.0.0'); + + final value = await parse({ + 'name': 'sample', + 'version': version.toString(), + 'publish_to': 'none', + 'author': 'name@example.com', + 'environment': {'sdk': sdkConstraint.toString()}, + 'description': 'description', + 'homepage': 'homepage', + 'documentation': 'documentation', + 'repository': 'https://github.com/example/repo', + 'issue_tracker': 'https://github.com/example/repo/issues', + 'funding': ['https://patreon.com/example'], + 'topics': ['widget', 'button'], + 'ignored_advisories': ['111', '222'], + 'screenshots': [ + {'description': 'my screenshot', 'path': 'path/to/screenshot'}, + ], + 'workspace': ['pkg1', 'pkg2'], + 'resolution': 'workspace', + 'executables': { + 'my_script': 'bin/my_script.dart', + 'my_script2': 'bin/my_script2.dart', + }, + 'dependencies': {'foo': '1.0.0'}, + 'dev_dependencies': {'bar': '2.0.0'}, + 'dependency_overrides': {'baz': '3.0.0'}, + }, skipTryPub: true); + + final jsonValue = value.toJson(); + + expect(jsonValue['name'], 'sample'); + expect(jsonValue['version'], version.toString()); + expect(jsonValue['publish_to'], 'none'); + expect(jsonValue['description'], 'description'); + expect(jsonValue['homepage'], 'homepage'); + expect(jsonValue['author'], 'name@example.com'); + expect(jsonValue['authors'], ['name@example.com']); + expect( + jsonValue['environment'], + containsPair('sdk', sdkConstraint.toString()), + ); + expect(jsonValue['documentation'], 'documentation'); + expect(jsonValue['dependencies'], hasLength(1)); + expect( + jsonValue['dependencies'], + containsPair('foo', {'version': '1.0.0'}), + ); + expect(jsonValue['dev_dependencies'], hasLength(1)); + expect( + jsonValue['dev_dependencies'], + containsPair('bar', {'version': '2.0.0'}), + ); + expect(jsonValue['dependency_overrides'], hasLength(1)); + expect( + jsonValue['dependency_overrides'], + containsPair('baz', {'version': '3.0.0'}), + ); + expect(jsonValue['repository'], 'https://github.com/example/repo'); + expect( + jsonValue['issue_tracker'], + 'https://github.com/example/repo/issues', + ); + expect(jsonValue['funding'], ['https://patreon.com/example']); + expect(jsonValue['topics'], ['widget', 'button']); + expect(jsonValue['ignored_advisories'], ['111', '222']); + expect(jsonValue['screenshots'], [ + {'description': 'my screenshot', 'path': 'path/to/screenshot'}, + ]); + expect(jsonValue['workspace'], ['pkg1', 'pkg2']); + expect(jsonValue['resolution'], 'workspace'); + expect(jsonValue['executables'], { + 'my_script': 'bin/my_script.dart', + 'my_script2': 'bin/my_script2.dart', + }); + }); + }); + + group('Pubspec round trip tests', () { + test('minimal set values', () async { + final value = await parse(defaultPubspec); + final jsonValue = value.toJson(); + final newValue = Pubspec.fromJson(jsonValue); + + expect(newValue.name, value.name); + expect(newValue.version, value.version); + expect(newValue.publishTo, value.publishTo); + expect(newValue.description, value.description); + expect(newValue.homepage, value.homepage); + expect(newValue.author, value.author); + expect(newValue.authors, value.authors); + expect(newValue.environment, value.environment); + expect(newValue.documentation, value.documentation); + expect(newValue.dependencies, value.dependencies); + expect(newValue.devDependencies, value.devDependencies); + expect(newValue.dependencyOverrides, value.dependencyOverrides); + expect(newValue.flutter, value.flutter); + expect(newValue.repository, value.repository); + expect(newValue.issueTracker, value.issueTracker); + expect(newValue.screenshots, value.screenshots); + expect(newValue.workspace, value.workspace); + expect(newValue.resolution, value.resolution); + expect(newValue.executables, value.executables); + }); + + test('all fields set', () async { + final version = Version.parse('1.2.3'); + final sdkConstraint = VersionConstraint.parse('>=3.6.0 <4.0.0'); + final value = await parse({ + 'name': 'sample', + 'version': version.toString(), + 'publish_to': 'none', + 'author': 'name@example.com', + 'environment': {'sdk': sdkConstraint.toString()}, + 'description': 'description', + 'homepage': 'homepage', + 'documentation': 'documentation', + 'repository': 'https://github.com/example/repo', + 'issue_tracker': 'https://github.com/example/repo/issues', + 'funding': ['https://patreon.com/example'], + 'topics': ['widget', 'button'], + 'ignored_advisories': ['111', '222'], + 'screenshots': [ + {'description': 'my screenshot', 'path': 'path/to/screenshot'}, + ], + 'workspace': ['pkg1', 'pkg2'], + 'resolution': 'workspace', + 'executables': { + 'my_script': 'bin/my_script.dart', + 'my_script2': 'bin/my_script2.dart', + }, + 'dependencies': {'foo': '1.0.0'}, + 'dev_dependencies': {'bar': '2.0.0'}, + 'dependency_overrides': {'baz': '3.0.0'}, + }, skipTryPub: true); + + final jsonValue = value.toJson(); + + final newValue = Pubspec.fromJson(jsonValue); + + expect(newValue.name, value.name); + expect(newValue.version, value.version); + expect(newValue.publishTo, value.publishTo); + expect(newValue.description, value.description); + expect(newValue.homepage, value.homepage); + expect(newValue.author, value.author); + expect(newValue.authors, value.authors); + expect(newValue.environment, value.environment); + expect(newValue.documentation, value.documentation); + expect(newValue.dependencies, value.dependencies); + expect(newValue.devDependencies, value.devDependencies); + expect(newValue.dependencyOverrides, value.dependencyOverrides); + expect(newValue.flutter, value.flutter); + expect(newValue.repository, value.repository); + expect(newValue.issueTracker, value.issueTracker); + expect(newValue.screenshots?.length, value.screenshots?.length); + expect( + newValue.screenshots?.first.description, + value.screenshots?.first.description, + ); + expect(newValue.screenshots?.first.path, value.screenshots?.first.path); + expect(newValue.workspace, value.workspace); + expect(newValue.resolution, value.resolution); + expect(newValue.executables, value.executables); + }); + + test('dependencies', () async { + final value = await parse({ + ...defaultPubspec, + 'dependencies': { + 'flutter': {'sdk': 'flutter'}, + 'http': '^1.1.0', + 'provider': {'version': '^6.0.5'}, + 'firebase_core': { + 'hosted': {'name': 'firebase_core', 'url': 'https://pub.dev'}, + 'version': '^2.13.0', + }, + 'google_fonts': {'sdk': 'flutter', 'version': '^4.0.3'}, + 'flutter_bloc': {'git': 'https://github.com/felangel/bloc.git'}, + 'shared_preferences': { + 'git': { + 'url': 'https://github.com/flutter/plugins.git', + 'ref': 'main', + 'path': 'packages/shared_preferences/shared_preferences', + }, + }, + 'local_utils': {'path': '../local_utils'}, + }, + }, skipTryPub: true); + + final jsonValue = value.toJson(); + + final newValue = await parse(jsonValue, skipTryPub: true); + + expect(value.dependencies, hasLength(8)); + expect(value.dependencies['flutter'], isA()); + expect(value.dependencies['http'], isA()); + expect(value.dependencies['provider'], isA()); + expect(value.dependencies['firebase_core'], isA()); + expect(value.dependencies['google_fonts'], isA()); + expect(value.dependencies['flutter_bloc'], isA()); + expect(value.dependencies['shared_preferences'], isA()); + expect(value.dependencies['local_utils'], isA()); + + expect( + value.dependencies['flutter']?.toJson(), + newValue.dependencies['flutter']?.toJson(), + ); + expect( + value.dependencies['http']?.toJson(), + newValue.dependencies['http']?.toJson(), + ); + expect( + value.dependencies['provider']?.toJson(), + newValue.dependencies['provider']?.toJson(), + ); + expect( + value.dependencies['firebase_core']?.toJson(), + newValue.dependencies['firebase_core']?.toJson(), + ); + expect( + value.dependencies['google_fonts']?.toJson(), + newValue.dependencies['google_fonts']?.toJson(), + ); + expect( + value.dependencies['flutter_bloc']?.toJson(), + newValue.dependencies['flutter_bloc']?.toJson(), + ); + expect( + value.dependencies['shared_preferences']?.toJson(), + newValue.dependencies['shared_preferences']?.toJson(), + ); + expect( + value.dependencies['local_utils']?.toJson(), + newValue.dependencies['local_utils']?.toJson(), + ); + }); + }); +} diff --git a/pkgs/watcher/test/directory_watcher/file_tests.dart b/pkgs/watcher/test/directory_watcher/file_tests.dart index 372ea4b27..9b3fcd6c2 100644 --- a/pkgs/watcher/test/directory_watcher/file_tests.dart +++ b/pkgs/watcher/test/directory_watcher/file_tests.dart @@ -11,13 +11,7 @@ import 'package:watcher/src/utils.dart'; import '../utils.dart'; -void fileTests({required bool isNative}) { - for (var i = 0; i != runsPerTest; ++i) { - _fileTests(isNative: isNative); - } -} - -void _fileTests({required bool isNative}) { +void fileTests() { test('does not notify for files that already exist when started', () async { // Make some pre-existing files. writeFile('a.txt'); @@ -147,6 +141,13 @@ void _fileTests({required bool isNative}) { }); }); + // Most of the time, when multiple filesystem actions happen in sequence, + // they'll be batched together and the watcher will see them all at once. + // These tests verify that the watcher normalizes and combine these events + // properly. However, very occasionally the events will be reported in + // separate batches, and the watcher will report them as though they occurred + // far apart in time, so each of these tests has a "backup case" to allow for + // that as well. group('clustered changes', () { test("doesn't notify when a file is created and then immediately removed", () async { @@ -154,6 +155,13 @@ void _fileTests({required bool isNative}) { await startWatcher(); writeFile('file.txt'); deleteFile('file.txt'); + + // Backup case. + startClosingEventStream(); + await allowEvents(() { + expectAddEvent('file.txt'); + expectRemoveEvent('file.txt'); + }); }); test( @@ -165,7 +173,13 @@ void _fileTests({required bool isNative}) { deleteFile('file.txt'); writeFile('file.txt', contents: 're-created'); - await expectModifyEvent('file.txt'); + await allowEither(() { + expectModifyEvent('file.txt'); + }, () { + // Backup case. + expectRemoveEvent('file.txt'); + expectAddEvent('file.txt'); + }); }); test( @@ -177,7 +191,14 @@ void _fileTests({required bool isNative}) { renameFile('old.txt', 'new.txt'); writeFile('old.txt', contents: 're-created'); - await inAnyOrder([isModifyEvent('old.txt'), isAddEvent('new.txt')]); + await allowEither(() { + inAnyOrder([isModifyEvent('old.txt'), isAddEvent('new.txt')]); + }, () { + // Backup case. + expectRemoveEvent('old.txt'); + expectAddEvent('new.txt'); + expectAddEvent('old.txt'); + }); }); test( @@ -189,6 +210,9 @@ void _fileTests({required bool isNative}) { writeFile('file.txt', contents: 'modified'); deleteFile('file.txt'); + // Backup case. + await allowModifyEvent('file.txt'); + await expectRemoveEvent('file.txt'); }); @@ -200,6 +224,10 @@ void _fileTests({required bool isNative}) { writeFile('file.txt', contents: 'modified'); await expectAddEvent('file.txt'); + + // Backup case. + startClosingEventStream(); + await allowModifyEvent('file.txt'); }); }); @@ -297,29 +325,6 @@ void _fileTests({required bool isNative}) { }))); }); - test( - 'emits events for many nested files moved out then immediately back in', - () async { - withPermutations( - (i, j, k) => writeFile('dir/sub/sub-$i/sub-$j/file-$k.txt')); - - await startWatcher(path: 'dir'); - - renameDir('dir/sub', 'sub'); - renameDir('sub', 'dir/sub'); - - if (isNative) { - await inAnyOrder(withPermutations( - (i, j, k) => isRemoveEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); - await inAnyOrder(withPermutations( - (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); - } else { - // Polling watchers can't detect this as directory contents mtimes - // aren't updated when the directory is moved. - await expectNoEvents(); - } - }); - test( 'emits events for many files added at once in a subdirectory with the ' 'same name as a removed file', () async { @@ -357,32 +362,4 @@ void _fileTests({required bool isNative}) { } }); }); - - test( - 'does not notify about the watched directory being deleted and ' - 'recreated immediately before watching', () async { - createDir('dir'); - writeFile('dir/old.txt'); - deleteDir('dir'); - createDir('dir'); - - await startWatcher(path: 'dir'); - writeFile('dir/newer.txt'); - await expectAddEvent('dir/newer.txt'); - }); - - test('does not suppress files with the same prefix as a directory', () async { - // Regression test for https://github.com/dart-lang/watcher/issues/83 - writeFile('some_name.txt'); - - await startWatcher(); - - writeFile('some_name/some_name.txt'); - deleteFile('some_name.txt'); - - await inAnyOrder([ - isAddEvent('some_name/some_name.txt'), - isRemoveEvent('some_name.txt') - ]); - }); } diff --git a/pkgs/watcher/test/directory_watcher/link_tests.dart b/pkgs/watcher/test/directory_watcher/link_tests.dart index c323c9e54..f4919d1fb 100644 --- a/pkgs/watcher/test/directory_watcher/link_tests.dart +++ b/pkgs/watcher/test/directory_watcher/link_tests.dart @@ -9,12 +9,6 @@ import 'package:test/test.dart'; import '../utils.dart'; void linkTests({required bool isNative}) { - for (var i = 0; i != runsPerTest; ++i) { - _linkTests(isNative: isNative); - } -} - -void _linkTests({required bool isNative}) { test('notifies when a link is added', () async { createDir('targets'); createDir('links'); diff --git a/pkgs/watcher/test/directory_watcher/linux_test.dart b/pkgs/watcher/test/directory_watcher/linux_test.dart index 948d565f9..a47779466 100644 --- a/pkgs/watcher/test/directory_watcher/linux_test.dart +++ b/pkgs/watcher/test/directory_watcher/linux_test.dart @@ -16,10 +16,31 @@ import 'link_tests.dart'; void main() { watcherFactory = LinuxDirectoryWatcher.new; - fileTests(isNative: true); + fileTests(); linkTests(isNative: true); test('DirectoryWatcher creates a LinuxDirectoryWatcher on Linux', () { expect(DirectoryWatcher('.'), const TypeMatcher()); }); + + test('emits events for many nested files moved out then immediately back in', + () async { + withPermutations( + (i, j, k) => writeFile('dir/sub/sub-$i/sub-$j/file-$k.txt')); + await startWatcher(path: 'dir'); + + renameDir('dir/sub', 'sub'); + renameDir('sub', 'dir/sub'); + + await allowEither(() { + inAnyOrder(withPermutations( + (i, j, k) => isRemoveEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); + + inAnyOrder(withPermutations( + (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); + }, () { + inAnyOrder(withPermutations( + (i, j, k) => isModifyEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); + }); + }); } diff --git a/pkgs/watcher/test/directory_watcher/mac_os_test.dart b/pkgs/watcher/test/directory_watcher/mac_os_test.dart index 1306cc4b3..ba52d1958 100644 --- a/pkgs/watcher/test/directory_watcher/mac_os_test.dart +++ b/pkgs/watcher/test/directory_watcher/mac_os_test.dart @@ -16,10 +16,56 @@ import 'link_tests.dart'; void main() { watcherFactory = MacOSDirectoryWatcher.new; - fileTests(isNative: true); + fileTests(); linkTests(isNative: true); test('DirectoryWatcher creates a MacOSDirectoryWatcher on Mac OS', () { expect(DirectoryWatcher('.'), const TypeMatcher()); }); + + test( + 'does not notify about the watched directory being deleted and ' + 'recreated immediately before watching', () async { + createDir('dir'); + writeFile('dir/old.txt'); + deleteDir('dir'); + createDir('dir'); + + await startWatcher(path: 'dir'); + writeFile('dir/newer.txt'); + await expectAddEvent('dir/newer.txt'); + }); + + test('emits events for many nested files moved out then immediately back in', + () async { + withPermutations( + (i, j, k) => writeFile('dir/sub/sub-$i/sub-$j/file-$k.txt')); + + await startWatcher(path: 'dir'); + + renameDir('dir/sub', 'sub'); + renameDir('sub', 'dir/sub'); + + await allowEither(() { + inAnyOrder(withPermutations( + (i, j, k) => isRemoveEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); + + inAnyOrder(withPermutations( + (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); + }, () { + inAnyOrder(withPermutations( + (i, j, k) => isModifyEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'))); + }); + }); + test('does not suppress files with the same prefix as a directory', () async { + // Regression test for https://github.com/dart-lang/watcher/issues/83 + writeFile('some_name.txt'); + + await startWatcher(); + + writeFile('some_name/some_name.txt'); + deleteFile('some_name.txt'); + + await expectRemoveEvent('some_name.txt'); + }); } diff --git a/pkgs/watcher/test/directory_watcher/polling_test.dart b/pkgs/watcher/test/directory_watcher/polling_test.dart index 90e8c2368..829ded36e 100644 --- a/pkgs/watcher/test/directory_watcher/polling_test.dart +++ b/pkgs/watcher/test/directory_watcher/polling_test.dart @@ -21,7 +21,7 @@ void main() { group('with mock mtime', () { setUp(enableMockModificationTimes); - fileTests(isNative: false); + fileTests(); linkTests(isNative: false); test('does not notify if the modification time did not change', () async { @@ -71,7 +71,7 @@ void main() { group('with real mtime', () { setUp(enableWaitingForDifferentModificationTimes); - fileTests(isNative: false); + fileTests(); linkTests(isNative: false); }); } diff --git a/pkgs/watcher/test/directory_watcher/windows_test.dart b/pkgs/watcher/test/directory_watcher/windows_test.dart index c4f6d44a0..406a0755e 100644 --- a/pkgs/watcher/test/directory_watcher/windows_test.dart +++ b/pkgs/watcher/test/directory_watcher/windows_test.dart @@ -21,7 +21,7 @@ import 'link_tests.dart'; void main() { watcherFactory = WindowsDirectoryWatcher.new; - fileTests(isNative: true); + fileTests(); linkTests(isNative: true); test('DirectoryWatcher creates a WindowsDirectoryWatcher on Windows', () { diff --git a/pkgs/watcher/test/file_watcher/file_tests.dart b/pkgs/watcher/test/file_watcher/file_tests.dart index 49d143879..74980f5f6 100644 --- a/pkgs/watcher/test/file_watcher/file_tests.dart +++ b/pkgs/watcher/test/file_watcher/file_tests.dart @@ -13,13 +13,11 @@ void fileTests({required bool isNative}) { writeFile('file.txt'); }); - for (var i = 0; i != runsPerTest; ++i) { - _fileTests(isNative: isNative); - } -} - -void _fileTests({required bool isNative}) { test("doesn't notify if the file isn't modified", () async { + // TODO(davidmorgan): fix startup race on MacOS. + if (isNative && Platform.isMacOS) { + await Future.delayed(const Duration(milliseconds: 100)); + } await startWatcher(path: 'file.txt'); await expectNoEvents(); }); diff --git a/pkgs/watcher/test/file_watcher/link_tests.dart b/pkgs/watcher/test/file_watcher/link_tests.dart index ddab12c47..8cb5e102d 100644 --- a/pkgs/watcher/test/file_watcher/link_tests.dart +++ b/pkgs/watcher/test/file_watcher/link_tests.dart @@ -12,12 +12,6 @@ void linkTests({required bool isNative}) { writeLink(link: 'link.txt', target: 'target.txt'); }); - for (var i = 0; i != runsPerTest; ++i) { - _linkTests(isNative: isNative); - } -} - -void _linkTests({required bool isNative}) { test("doesn't notify if nothing is modified", () async { await startWatcher(path: 'link.txt'); await expectNoEvents(); diff --git a/pkgs/watcher/test/utils.dart b/pkgs/watcher/test/utils.dart index 9a41b5ba6..354387c56 100644 --- a/pkgs/watcher/test/utils.dart +++ b/pkgs/watcher/test/utils.dart @@ -12,9 +12,6 @@ import 'package:test_descriptor/test_descriptor.dart' as d; import 'package:watcher/src/stat.dart'; import 'package:watcher/watcher.dart'; -/// Edit this to run fast-running tests many times. -int runsPerTest = 1; - typedef WatcherFactory = Watcher Function(String directory); /// Sets the function used to create the watcher. @@ -167,18 +164,62 @@ void startClosingEventStream() async { await _watcherEvents.cancel(immediate: true); } -/// Add [streamMatcher] as an expectation to [_watcherEvents]. +/// A list of [StreamMatcher]s that have been collected using +/// [_collectStreamMatcher]. +List? _collectedStreamMatchers; + +/// Collects all stream matchers that are registered within [block] into a +/// single stream matcher. +/// +/// The returned matcher will match each of the collected matchers in order. +StreamMatcher _collectStreamMatcher(void Function() block) { + var oldStreamMatchers = _collectedStreamMatchers; + var collectedStreamMatchers = _collectedStreamMatchers = []; + try { + block(); + return emitsInOrder(collectedStreamMatchers); + } finally { + _collectedStreamMatchers = oldStreamMatchers; + } +} + +/// Either add [streamMatcher] as an expectation to [_watcherEvents], or collect +/// it with [_collectStreamMatcher]. /// /// [streamMatcher] can be a [StreamMatcher], a [Matcher], or a value. -Future _expect(Matcher streamMatcher) { - return expectLater(_watcherEvents, emits(streamMatcher)); +Future _expectOrCollect(Matcher streamMatcher) { + var collectedStreamMatchers = _collectedStreamMatchers; + if (collectedStreamMatchers != null) { + collectedStreamMatchers.add(emits(streamMatcher)); + return Future.sync(() {}); + } else { + return expectLater(_watcherEvents, emits(streamMatcher)); + } } /// Expects that [matchers] will match emitted events in any order. /// /// [matchers] may be [Matcher]s or values, but not [StreamMatcher]s. -Future inAnyOrder(Iterable matchers) => - _expect(emitsInAnyOrder(matchers.toSet())); +Future inAnyOrder(Iterable matchers) { + matchers = matchers.toSet(); + return _expectOrCollect(emitsInAnyOrder(matchers)); +} + +/// Expects that the expectations established in either [block1] or [block2] +/// will match the emitted events. +/// +/// If both blocks match, the one that consumed more events will be used. +Future allowEither(void Function() block1, void Function() block2) => + _expectOrCollect(emitsAnyOf( + [_collectStreamMatcher(block1), _collectStreamMatcher(block2)])); + +/// Allows the expectations established in [block] to match the emitted events. +/// +/// If the expectations in [block] don't match, no error will be raised and no +/// events will be consumed. If this is used at the end of a test, +/// [startClosingEventStream] should be called before it. +Future allowEvents(void Function() block) => + _expectOrCollect(mayEmit(_collectStreamMatcher(block))); /// Returns a StreamMatcher that matches a [WatchEvent] with the given [type] /// and [path]. @@ -233,16 +274,24 @@ Future> takeEvents({required Duration duration}) async { /// Expects that the next event emitted will be for an add event for [path]. Future expectAddEvent(String path) => - _expect(isWatchEvent(ChangeType.ADD, path)); + _expectOrCollect(isWatchEvent(ChangeType.ADD, path)); /// Expects that the next event emitted will be for a modification event for /// [path]. Future expectModifyEvent(String path) => - _expect(isWatchEvent(ChangeType.MODIFY, path)); + _expectOrCollect(isWatchEvent(ChangeType.MODIFY, path)); /// Expects that the next event emitted will be for a removal event for [path]. Future expectRemoveEvent(String path) => - _expect(isWatchEvent(ChangeType.REMOVE, path)); + _expectOrCollect(isWatchEvent(ChangeType.REMOVE, path)); + +/// Consumes a modification event for [path] if one is emitted at this point in +/// the schedule, but doesn't throw an error if it isn't. +/// +/// If this is used at the end of a test, [startClosingEventStream] should be +/// called before it. +Future allowModifyEvent(String path) => + _expectOrCollect(mayEmit(isWatchEvent(ChangeType.MODIFY, path))); /// Track a fake timestamp to be used when writing files. This always increases /// so that files that are deleted and re-created do not have their timestamp From 421b6cf0f801252af03c56e9545ab4fce4e0efc7 Mon Sep 17 00:00:00 2001 From: Dhruv Maradiya Date: Sun, 19 Oct 2025 17:15:53 +0530 Subject: [PATCH 2/5] Reformat: formatted the code with dart format --- pkgs/pubspec_parse/lib/src/dependency.dart | 38 ++-- pkgs/pubspec_parse/lib/src/pubspec.dart | 23 +-- pkgs/pubspec_parse/lib/src/pubspec.g.dart | 211 +++++++++++---------- pkgs/pubspec_parse/test/parse_test.dart | 14 +- pkgs/pubspec_parse/test/test_utils.dart | 41 ++-- 5 files changed, 167 insertions(+), 160 deletions(-) diff --git a/pkgs/pubspec_parse/lib/src/dependency.dart b/pkgs/pubspec_parse/lib/src/dependency.dart index 4658ac38e..983824a57 100644 --- a/pkgs/pubspec_parse/lib/src/dependency.dart +++ b/pkgs/pubspec_parse/lib/src/dependency.dart @@ -46,10 +46,8 @@ Dependency? _fromJson(Object? data, String name) { } if (data is Map) { - final matchedKeys = data.keys - .cast() - .where((key) => key != 'version') - .toList(); + final matchedKeys = + data.keys.cast().where((key) => key != 'version').toList(); if (data.isEmpty || (matchedKeys.isEmpty && data.containsKey('version'))) { return _$HostedDependencyFromJson(data); @@ -82,8 +80,8 @@ Dependency? _fromJson(Object? data, String name) { 'path' => PathDependency.fromData(data[key]), 'sdk' => _$SdkDependencyFromJson(data), 'hosted' => _$HostedDependencyFromJson( - data, - )..hosted?._nameOfPackage = name, + data, + )..hosted?._nameOfPackage = name, _ => throw StateError('There is a bug in pubspec_parse.'), }; }); @@ -105,7 +103,7 @@ class SdkDependency extends Dependency { final VersionConstraint version; SdkDependency(this.sdk, {VersionConstraint? version}) - : version = version ?? VersionConstraint.any; + : version = version ?? VersionConstraint.any; @override bool operator ==(Object other) => @@ -157,12 +155,12 @@ class GitDependency extends Dependency { @override Map toJson() => { - 'git': { - 'url': url.toString(), - if (ref != null) 'ref': ref, - if (path != null) 'path': path, - }, - }; + 'git': { + 'url': url.toString(), + if (ref != null) 'ref': ref, + if (path != null) 'path': path, + }, + }; } Uri? parseGitUriOrNull(String? value) => @@ -236,7 +234,7 @@ class HostedDependency extends Dependency { final HostedDetails? hosted; HostedDependency({VersionConstraint? version, this.hosted}) - : version = version ?? VersionConstraint.any; + : version = version ?? VersionConstraint.any; @override bool operator ==(Object other) => @@ -252,9 +250,9 @@ class HostedDependency extends Dependency { @override Map toJson() => { - 'version': version.toString(), - if (hosted != null) 'hosted': hosted!.toJson(), - }; + 'version': version.toString(), + if (hosted != null) 'hosted': hosted!.toJson(), + }; } @JsonSerializable(disallowUnrecognizedKeys: true) @@ -300,9 +298,9 @@ class HostedDetails { int get hashCode => Object.hash(name, url); Map toJson() => { - if (declaredName != null) 'name': declaredName, - 'url': url.toString(), - }; + if (declaredName != null) 'name': declaredName, + 'url': url.toString(), + }; } VersionConstraint _constraintFromString(String? input) => diff --git a/pkgs/pubspec_parse/lib/src/pubspec.dart b/pkgs/pubspec_parse/lib/src/pubspec.dart index b8c2aea71..a2c585860 100644 --- a/pkgs/pubspec_parse/lib/src/pubspec.dart +++ b/pkgs/pubspec_parse/lib/src/pubspec.dart @@ -126,16 +126,16 @@ class Pubspec { Map? dependencyOverrides, this.flutter, Map? executables, - }) : authors // ignore: deprecated_member_use_from_same_package - = _normalizeAuthors( - author, - authors, - ), - environment = environment ?? const {}, - dependencies = dependencies ?? const {}, - devDependencies = devDependencies ?? const {}, - executables = executables ?? const {}, - dependencyOverrides = dependencyOverrides ?? const {} { + }) : authors // ignore: deprecated_member_use_from_same_package + = _normalizeAuthors( + author, + authors, + ), + environment = environment ?? const {}, + dependencies = dependencies ?? const {}, + devDependencies = devDependencies ?? const {}, + executables = executables ?? const {}, + dependencyOverrides = dependencyOverrides ?? const {} { if (name.isEmpty) { throw ArgumentError.value(name, 'name', '"name" cannot be empty.'); } @@ -253,7 +253,8 @@ Map _executablesMap(Map? source) => Map _serializeEnvironment( Map map, -) => map.map((key, value) => MapEntry(key, value?.toString())); +) => + map.map((key, value) => MapEntry(key, value?.toString())); String? _versionToString(Version? version) => version?.toString(); diff --git a/pkgs/pubspec_parse/lib/src/pubspec.g.dart b/pkgs/pubspec_parse/lib/src/pubspec.g.dart index 326f5f60a..5529ad00f 100644 --- a/pkgs/pubspec_parse/lib/src/pubspec.g.dart +++ b/pkgs/pubspec_parse/lib/src/pubspec.g.dart @@ -9,111 +9,112 @@ part of 'pubspec.dart'; // ************************************************************************** Pubspec _$PubspecFromJson(Map json) => $checkedCreate( - 'Pubspec', - json, - ($checkedConvert) { - final val = Pubspec( - $checkedConvert('name', (v) => v as String), - version: $checkedConvert( - 'version', - (v) => _versionFromString(v as String?), - ), - publishTo: $checkedConvert('publish_to', (v) => v as String?), - author: $checkedConvert('author', (v) => v as String?), - authors: $checkedConvert( - 'authors', - (v) => (v as List?)?.map((e) => e as String).toList(), - ), - environment: $checkedConvert( - 'environment', - (v) => _environmentMap(v as Map?), - ), - homepage: $checkedConvert('homepage', (v) => v as String?), - repository: $checkedConvert( - 'repository', - (v) => v == null ? null : Uri.parse(v as String), - ), - issueTracker: $checkedConvert( - 'issue_tracker', - (v) => v == null ? null : Uri.parse(v as String), - ), - funding: $checkedConvert( - 'funding', - (v) => - (v as List?)?.map((e) => Uri.parse(e as String)).toList(), - ), - topics: $checkedConvert( - 'topics', - (v) => (v as List?)?.map((e) => e as String).toList(), - ), - ignoredAdvisories: $checkedConvert( - 'ignored_advisories', - (v) => (v as List?)?.map((e) => e as String).toList(), - ), - screenshots: $checkedConvert( - 'screenshots', - (v) => parseScreenshots(v as List?), - ), - documentation: $checkedConvert('documentation', (v) => v as String?), - description: $checkedConvert('description', (v) => v as String?), - workspace: $checkedConvert( - 'workspace', - (v) => (v as List?)?.map((e) => e as String).toList(), - ), - resolution: $checkedConvert('resolution', (v) => v as String?), - dependencies: $checkedConvert( - 'dependencies', - (v) => parseDeps(v as Map?), - ), - devDependencies: $checkedConvert( - 'dev_dependencies', - (v) => parseDeps(v as Map?), - ), - dependencyOverrides: $checkedConvert( - 'dependency_overrides', - (v) => parseDeps(v as Map?), - ), - flutter: $checkedConvert( - 'flutter', - (v) => (v as Map?)?.map((k, e) => MapEntry(k as String, e)), - ), - executables: $checkedConvert( - 'executables', - (v) => _executablesMap(v as Map?), - ), + 'Pubspec', + json, + ($checkedConvert) { + final val = Pubspec( + $checkedConvert('name', (v) => v as String), + version: $checkedConvert( + 'version', + (v) => _versionFromString(v as String?), + ), + publishTo: $checkedConvert('publish_to', (v) => v as String?), + author: $checkedConvert('author', (v) => v as String?), + authors: $checkedConvert( + 'authors', + (v) => (v as List?)?.map((e) => e as String).toList(), + ), + environment: $checkedConvert( + 'environment', + (v) => _environmentMap(v as Map?), + ), + homepage: $checkedConvert('homepage', (v) => v as String?), + repository: $checkedConvert( + 'repository', + (v) => v == null ? null : Uri.parse(v as String), + ), + issueTracker: $checkedConvert( + 'issue_tracker', + (v) => v == null ? null : Uri.parse(v as String), + ), + funding: $checkedConvert( + 'funding', + (v) => (v as List?) + ?.map((e) => Uri.parse(e as String)) + .toList(), + ), + topics: $checkedConvert( + 'topics', + (v) => (v as List?)?.map((e) => e as String).toList(), + ), + ignoredAdvisories: $checkedConvert( + 'ignored_advisories', + (v) => (v as List?)?.map((e) => e as String).toList(), + ), + screenshots: $checkedConvert( + 'screenshots', + (v) => parseScreenshots(v as List?), + ), + documentation: $checkedConvert('documentation', (v) => v as String?), + description: $checkedConvert('description', (v) => v as String?), + workspace: $checkedConvert( + 'workspace', + (v) => (v as List?)?.map((e) => e as String).toList(), + ), + resolution: $checkedConvert('resolution', (v) => v as String?), + dependencies: $checkedConvert( + 'dependencies', + (v) => parseDeps(v as Map?), + ), + devDependencies: $checkedConvert( + 'dev_dependencies', + (v) => parseDeps(v as Map?), + ), + dependencyOverrides: $checkedConvert( + 'dependency_overrides', + (v) => parseDeps(v as Map?), + ), + flutter: $checkedConvert( + 'flutter', + (v) => (v as Map?)?.map((k, e) => MapEntry(k as String, e)), + ), + executables: $checkedConvert( + 'executables', + (v) => _executablesMap(v as Map?), + ), + ); + return val; + }, + fieldKeyMap: const { + 'publishTo': 'publish_to', + 'issueTracker': 'issue_tracker', + 'ignoredAdvisories': 'ignored_advisories', + 'devDependencies': 'dev_dependencies', + 'dependencyOverrides': 'dependency_overrides', + }, ); - return val; - }, - fieldKeyMap: const { - 'publishTo': 'publish_to', - 'issueTracker': 'issue_tracker', - 'ignoredAdvisories': 'ignored_advisories', - 'devDependencies': 'dev_dependencies', - 'dependencyOverrides': 'dependency_overrides', - }, -); Map _$PubspecToJson(Pubspec instance) => { - 'name': instance.name, - 'version': _versionToString(instance.version), - 'description': instance.description, - 'homepage': instance.homepage, - 'publish_to': instance.publishTo, - 'repository': instance.repository?.toString(), - 'issue_tracker': instance.issueTracker?.toString(), - 'funding': instance.funding?.map((e) => e.toString()).toList(), - 'topics': instance.topics, - 'ignored_advisories': instance.ignoredAdvisories, - 'screenshots': serializeScreenshots(instance.screenshots), - 'author': instance.author, - 'authors': instance.authors, - 'documentation': instance.documentation, - 'environment': _serializeEnvironment(instance.environment), - 'dependencies': serializeDeps(instance.dependencies), - 'dev_dependencies': serializeDeps(instance.devDependencies), - 'dependency_overrides': serializeDeps(instance.dependencyOverrides), - 'flutter': instance.flutter, - 'executables': _serializeExecutables(instance.executables), - 'workspace': instance.workspace, - 'resolution': instance.resolution, -}; + 'name': instance.name, + 'version': _versionToString(instance.version), + 'description': instance.description, + 'homepage': instance.homepage, + 'publish_to': instance.publishTo, + 'repository': instance.repository?.toString(), + 'issue_tracker': instance.issueTracker?.toString(), + 'funding': instance.funding?.map((e) => e.toString()).toList(), + 'topics': instance.topics, + 'ignored_advisories': instance.ignoredAdvisories, + 'screenshots': serializeScreenshots(instance.screenshots), + 'author': instance.author, + 'authors': instance.authors, + 'documentation': instance.documentation, + 'environment': _serializeEnvironment(instance.environment), + 'dependencies': serializeDeps(instance.dependencies), + 'dev_dependencies': serializeDeps(instance.devDependencies), + 'dependency_overrides': serializeDeps(instance.dependencyOverrides), + 'flutter': instance.flutter, + 'executables': _serializeExecutables(instance.executables), + 'workspace': instance.workspace, + 'resolution': instance.resolution, + }; diff --git a/pkgs/pubspec_parse/test/parse_test.dart b/pkgs/pubspec_parse/test/parse_test.dart index f700b4efa..613dae4fc 100644 --- a/pkgs/pubspec_parse/test/parse_test.dart +++ b/pkgs/pubspec_parse/test/parse_test.dart @@ -651,21 +651,27 @@ line 9, column 12: Unsupported value for "path". `42` is not a String group('lenient', () { test('null', () { - expectParseThrows(null, r''' + expectParseThrows( + null, + r''' line 1, column 1: Not a map ╷ 1 │ null │ ^^^^ - ╵''', lenient: true); + ╵''', + lenient: true); }); test('empty string', () { - expectParseThrows('', r''' + expectParseThrows( + '', + r''' line 1, column 1: Not a map ╷ 1 │ "" │ ^^ - ╵''', lenient: true); + ╵''', + lenient: true); }); test('name cannot be empty', () { diff --git a/pkgs/pubspec_parse/test/test_utils.dart b/pkgs/pubspec_parse/test/test_utils.dart index e04c41088..b0b5ef7d2 100644 --- a/pkgs/pubspec_parse/test/test_utils.dart +++ b/pkgs/pubspec_parse/test/test_utils.dart @@ -21,17 +21,17 @@ String _encodeJson(Object? input) => const JsonEncoder.withIndent(' ').convert(input); Matcher _throwsParsedYamlException(String prettyValue) => throwsA( - const TypeMatcher().having( - (e) { - final message = e.formattedMessage; - printOnFailure("Actual error format:\nr'''\n$message'''"); - _printDebugParsedYamlException(e); - return message; - }, - 'formattedMessage', - prettyValue, - ), -); + const TypeMatcher().having( + (e) { + final message = e.formattedMessage; + printOnFailure("Actual error format:\nr'''\n$message'''"); + _printDebugParsedYamlException(e); + return message; + }, + 'formattedMessage', + prettyValue, + ), + ); void _printDebugParsedYamlException(ParsedYamlException e) { var innerError = e.innerError; @@ -113,15 +113,16 @@ void expectParseThrows( String expectedError, { bool skipTryPub = false, bool lenient = false, -}) => expect( - () => parse( - content, - lenient: lenient, - quietOnError: true, - skipTryPub: skipTryPub, - ), - _throwsParsedYamlException(expectedError), -); +}) => + expect( + () => parse( + content, + lenient: lenient, + quietOnError: true, + skipTryPub: skipTryPub, + ), + _throwsParsedYamlException(expectedError), + ); void expectParseThrowsContaining( Object? content, From e6058dafe4609dc2558b6651cdd7449a65b5d7a7 Mon Sep 17 00:00:00 2001 From: Dhruv Maradiya Date: Fri, 24 Oct 2025 18:58:09 +0530 Subject: [PATCH 3/5] Refactor serialization helpers and simplify collection literals - Make serializeDeps private (_serializeDeps) - Use concise literals (for-in, null-aware spread) in screenshots, executables, and deps - Clarify _fromJson docs and tidy formatting across dependency/pubspec files - Regenerate pubspec.g.dart for updated serializer and formatting --- pkgs/pubspec_parse/lib/src/dependency.dart | 42 ++-- pkgs/pubspec_parse/lib/src/pubspec.dart | 38 ++-- pkgs/pubspec_parse/lib/src/pubspec.g.dart | 211 ++++++++++----------- pkgs/pubspec_parse/lib/src/screenshot.dart | 11 +- pkgs/pubspec_parse/test/parse_test.dart | 14 +- pkgs/pubspec_parse/test/test_utils.dart | 41 ++-- 6 files changed, 173 insertions(+), 184 deletions(-) diff --git a/pkgs/pubspec_parse/lib/src/dependency.dart b/pkgs/pubspec_parse/lib/src/dependency.dart index 983824a57..dd1eed371 100644 --- a/pkgs/pubspec_parse/lib/src/dependency.dart +++ b/pkgs/pubspec_parse/lib/src/dependency.dart @@ -39,15 +39,19 @@ Map parseDeps(Map? source) => const _sourceKeys = ['sdk', 'git', 'path', 'hosted']; -/// Returns `null` if the data could not be parsed. +/// Converts [data] into a [Dependency] object. +/// If [data] is not a valid representation of a dependency, +/// returns null so that the parent logic can throw the proper error. Dependency? _fromJson(Object? data, String name) { if (data is String || data == null) { return _$HostedDependencyFromJson({'version': data}); } if (data is Map) { - final matchedKeys = - data.keys.cast().where((key) => key != 'version').toList(); + final matchedKeys = data.keys + .cast() + .where((key) => key != 'version') + .toList(); if (data.isEmpty || (matchedKeys.isEmpty && data.containsKey('version'))) { return _$HostedDependencyFromJson(data); @@ -80,15 +84,14 @@ Dependency? _fromJson(Object? data, String name) { 'path' => PathDependency.fromData(data[key]), 'sdk' => _$SdkDependencyFromJson(data), 'hosted' => _$HostedDependencyFromJson( - data, - )..hosted?._nameOfPackage = name, + data, + )..hosted?._nameOfPackage = name, _ => throw StateError('There is a bug in pubspec_parse.'), }; }); } } - // Not a String or a Map – return null so parent logic can throw proper error return null; } @@ -103,7 +106,7 @@ class SdkDependency extends Dependency { final VersionConstraint version; SdkDependency(this.sdk, {VersionConstraint? version}) - : version = version ?? VersionConstraint.any; + : version = version ?? VersionConstraint.any; @override bool operator ==(Object other) => @@ -155,12 +158,8 @@ class GitDependency extends Dependency { @override Map toJson() => { - 'git': { - 'url': url.toString(), - if (ref != null) 'ref': ref, - if (path != null) 'path': path, - }, - }; + 'git': {'url': url.toString(), 'ref': ?ref, 'path': ?path}, + }; } Uri? parseGitUriOrNull(String? value) => @@ -234,7 +233,7 @@ class HostedDependency extends Dependency { final HostedDetails? hosted; HostedDependency({VersionConstraint? version, this.hosted}) - : version = version ?? VersionConstraint.any; + : version = version ?? VersionConstraint.any; @override bool operator ==(Object other) => @@ -250,9 +249,9 @@ class HostedDependency extends Dependency { @override Map toJson() => { - 'version': version.toString(), - if (hosted != null) 'hosted': hosted!.toJson(), - }; + 'version': version.toString(), + if (hosted != null) 'hosted': hosted!.toJson(), + }; } @JsonSerializable(disallowUnrecognizedKeys: true) @@ -298,13 +297,10 @@ class HostedDetails { int get hashCode => Object.hash(name, url); Map toJson() => { - if (declaredName != null) 'name': declaredName, - 'url': url.toString(), - }; + if (declaredName != null) 'name': declaredName, + 'url': url.toString(), + }; } VersionConstraint _constraintFromString(String? input) => input == null ? VersionConstraint.any : VersionConstraint.parse(input); - -Map serializeDeps(Map input) => - input.map((k, v) => MapEntry(k, v.toJson())); diff --git a/pkgs/pubspec_parse/lib/src/pubspec.dart b/pkgs/pubspec_parse/lib/src/pubspec.dart index a2c585860..309c0060a 100644 --- a/pkgs/pubspec_parse/lib/src/pubspec.dart +++ b/pkgs/pubspec_parse/lib/src/pubspec.dart @@ -72,13 +72,13 @@ class Pubspec { @JsonKey(fromJson: _environmentMap, toJson: _serializeEnvironment) final Map environment; - @JsonKey(fromJson: parseDeps, toJson: serializeDeps) + @JsonKey(fromJson: parseDeps, toJson: _serializeDeps) final Map dependencies; - @JsonKey(fromJson: parseDeps, toJson: serializeDeps) + @JsonKey(fromJson: parseDeps, toJson: _serializeDeps) final Map devDependencies; - @JsonKey(fromJson: parseDeps, toJson: serializeDeps) + @JsonKey(fromJson: parseDeps, toJson: _serializeDeps) final Map dependencyOverrides; /// Optional configuration specific to [Flutter](https://flutter.io/) @@ -126,16 +126,16 @@ class Pubspec { Map? dependencyOverrides, this.flutter, Map? executables, - }) : authors // ignore: deprecated_member_use_from_same_package - = _normalizeAuthors( - author, - authors, - ), - environment = environment ?? const {}, - dependencies = dependencies ?? const {}, - devDependencies = devDependencies ?? const {}, - executables = executables ?? const {}, - dependencyOverrides = dependencyOverrides ?? const {} { + }) : authors // ignore: deprecated_member_use_from_same_package + = _normalizeAuthors( + author, + authors, + ), + environment = environment ?? const {}, + dependencies = dependencies ?? const {}, + devDependencies = devDependencies ?? const {}, + executables = executables ?? const {}, + dependencyOverrides = dependencyOverrides ?? const {} { if (name.isEmpty) { throw ArgumentError.value(name, 'name', '"name" cannot be empty.'); } @@ -253,10 +253,14 @@ Map _executablesMap(Map? source) => Map _serializeEnvironment( Map map, -) => - map.map((key, value) => MapEntry(key, value?.toString())); +) => map.map((key, value) => MapEntry(key, value?.toString())); String? _versionToString(Version? version) => version?.toString(); -Map _serializeExecutables(Map? map) => - map?.map(MapEntry.new) ?? {}; +Map _serializeExecutables(Map? map) => { + ...?map, +}; + +Map _serializeDeps(Map input) => { + for (final MapEntry(:key, :value) in input.entries) key: value.toJson(), +}; diff --git a/pkgs/pubspec_parse/lib/src/pubspec.g.dart b/pkgs/pubspec_parse/lib/src/pubspec.g.dart index 5529ad00f..cfbab518e 100644 --- a/pkgs/pubspec_parse/lib/src/pubspec.g.dart +++ b/pkgs/pubspec_parse/lib/src/pubspec.g.dart @@ -9,112 +9,111 @@ part of 'pubspec.dart'; // ************************************************************************** Pubspec _$PubspecFromJson(Map json) => $checkedCreate( - 'Pubspec', - json, - ($checkedConvert) { - final val = Pubspec( - $checkedConvert('name', (v) => v as String), - version: $checkedConvert( - 'version', - (v) => _versionFromString(v as String?), - ), - publishTo: $checkedConvert('publish_to', (v) => v as String?), - author: $checkedConvert('author', (v) => v as String?), - authors: $checkedConvert( - 'authors', - (v) => (v as List?)?.map((e) => e as String).toList(), - ), - environment: $checkedConvert( - 'environment', - (v) => _environmentMap(v as Map?), - ), - homepage: $checkedConvert('homepage', (v) => v as String?), - repository: $checkedConvert( - 'repository', - (v) => v == null ? null : Uri.parse(v as String), - ), - issueTracker: $checkedConvert( - 'issue_tracker', - (v) => v == null ? null : Uri.parse(v as String), - ), - funding: $checkedConvert( - 'funding', - (v) => (v as List?) - ?.map((e) => Uri.parse(e as String)) - .toList(), - ), - topics: $checkedConvert( - 'topics', - (v) => (v as List?)?.map((e) => e as String).toList(), - ), - ignoredAdvisories: $checkedConvert( - 'ignored_advisories', - (v) => (v as List?)?.map((e) => e as String).toList(), - ), - screenshots: $checkedConvert( - 'screenshots', - (v) => parseScreenshots(v as List?), - ), - documentation: $checkedConvert('documentation', (v) => v as String?), - description: $checkedConvert('description', (v) => v as String?), - workspace: $checkedConvert( - 'workspace', - (v) => (v as List?)?.map((e) => e as String).toList(), - ), - resolution: $checkedConvert('resolution', (v) => v as String?), - dependencies: $checkedConvert( - 'dependencies', - (v) => parseDeps(v as Map?), - ), - devDependencies: $checkedConvert( - 'dev_dependencies', - (v) => parseDeps(v as Map?), - ), - dependencyOverrides: $checkedConvert( - 'dependency_overrides', - (v) => parseDeps(v as Map?), - ), - flutter: $checkedConvert( - 'flutter', - (v) => (v as Map?)?.map((k, e) => MapEntry(k as String, e)), - ), - executables: $checkedConvert( - 'executables', - (v) => _executablesMap(v as Map?), - ), - ); - return val; - }, - fieldKeyMap: const { - 'publishTo': 'publish_to', - 'issueTracker': 'issue_tracker', - 'ignoredAdvisories': 'ignored_advisories', - 'devDependencies': 'dev_dependencies', - 'dependencyOverrides': 'dependency_overrides', - }, + 'Pubspec', + json, + ($checkedConvert) { + final val = Pubspec( + $checkedConvert('name', (v) => v as String), + version: $checkedConvert( + 'version', + (v) => _versionFromString(v as String?), + ), + publishTo: $checkedConvert('publish_to', (v) => v as String?), + author: $checkedConvert('author', (v) => v as String?), + authors: $checkedConvert( + 'authors', + (v) => (v as List?)?.map((e) => e as String).toList(), + ), + environment: $checkedConvert( + 'environment', + (v) => _environmentMap(v as Map?), + ), + homepage: $checkedConvert('homepage', (v) => v as String?), + repository: $checkedConvert( + 'repository', + (v) => v == null ? null : Uri.parse(v as String), + ), + issueTracker: $checkedConvert( + 'issue_tracker', + (v) => v == null ? null : Uri.parse(v as String), + ), + funding: $checkedConvert( + 'funding', + (v) => + (v as List?)?.map((e) => Uri.parse(e as String)).toList(), + ), + topics: $checkedConvert( + 'topics', + (v) => (v as List?)?.map((e) => e as String).toList(), + ), + ignoredAdvisories: $checkedConvert( + 'ignored_advisories', + (v) => (v as List?)?.map((e) => e as String).toList(), + ), + screenshots: $checkedConvert( + 'screenshots', + (v) => parseScreenshots(v as List?), + ), + documentation: $checkedConvert('documentation', (v) => v as String?), + description: $checkedConvert('description', (v) => v as String?), + workspace: $checkedConvert( + 'workspace', + (v) => (v as List?)?.map((e) => e as String).toList(), + ), + resolution: $checkedConvert('resolution', (v) => v as String?), + dependencies: $checkedConvert( + 'dependencies', + (v) => parseDeps(v as Map?), + ), + devDependencies: $checkedConvert( + 'dev_dependencies', + (v) => parseDeps(v as Map?), + ), + dependencyOverrides: $checkedConvert( + 'dependency_overrides', + (v) => parseDeps(v as Map?), + ), + flutter: $checkedConvert( + 'flutter', + (v) => (v as Map?)?.map((k, e) => MapEntry(k as String, e)), + ), + executables: $checkedConvert( + 'executables', + (v) => _executablesMap(v as Map?), + ), ); + return val; + }, + fieldKeyMap: const { + 'publishTo': 'publish_to', + 'issueTracker': 'issue_tracker', + 'ignoredAdvisories': 'ignored_advisories', + 'devDependencies': 'dev_dependencies', + 'dependencyOverrides': 'dependency_overrides', + }, +); Map _$PubspecToJson(Pubspec instance) => { - 'name': instance.name, - 'version': _versionToString(instance.version), - 'description': instance.description, - 'homepage': instance.homepage, - 'publish_to': instance.publishTo, - 'repository': instance.repository?.toString(), - 'issue_tracker': instance.issueTracker?.toString(), - 'funding': instance.funding?.map((e) => e.toString()).toList(), - 'topics': instance.topics, - 'ignored_advisories': instance.ignoredAdvisories, - 'screenshots': serializeScreenshots(instance.screenshots), - 'author': instance.author, - 'authors': instance.authors, - 'documentation': instance.documentation, - 'environment': _serializeEnvironment(instance.environment), - 'dependencies': serializeDeps(instance.dependencies), - 'dev_dependencies': serializeDeps(instance.devDependencies), - 'dependency_overrides': serializeDeps(instance.dependencyOverrides), - 'flutter': instance.flutter, - 'executables': _serializeExecutables(instance.executables), - 'workspace': instance.workspace, - 'resolution': instance.resolution, - }; + 'name': instance.name, + 'version': _versionToString(instance.version), + 'description': instance.description, + 'homepage': instance.homepage, + 'publish_to': instance.publishTo, + 'repository': instance.repository?.toString(), + 'issue_tracker': instance.issueTracker?.toString(), + 'funding': instance.funding?.map((e) => e.toString()).toList(), + 'topics': instance.topics, + 'ignored_advisories': instance.ignoredAdvisories, + 'screenshots': serializeScreenshots(instance.screenshots), + 'author': instance.author, + 'authors': instance.authors, + 'documentation': instance.documentation, + 'environment': _serializeEnvironment(instance.environment), + 'dependencies': _serializeDeps(instance.dependencies), + 'dev_dependencies': _serializeDeps(instance.devDependencies), + 'dependency_overrides': _serializeDeps(instance.dependencyOverrides), + 'flutter': instance.flutter, + 'executables': _serializeExecutables(instance.executables), + 'workspace': instance.workspace, + 'resolution': instance.resolution, +}; diff --git a/pkgs/pubspec_parse/lib/src/screenshot.dart b/pkgs/pubspec_parse/lib/src/screenshot.dart index 1ca861367..76559f359 100644 --- a/pkgs/pubspec_parse/lib/src/screenshot.dart +++ b/pkgs/pubspec_parse/lib/src/screenshot.dart @@ -64,10 +64,7 @@ List parseScreenshots(List? input) { return res; } -List> serializeScreenshots(List? input) => - input - ?.map( - (e) => {'description': e.description, 'path': e.path}, - ) - .toList() ?? - []; +List> serializeScreenshots(List? input) => [ + if (input != null) + for (var e in input) {'description': e.description, 'path': e.path}, +]; diff --git a/pkgs/pubspec_parse/test/parse_test.dart b/pkgs/pubspec_parse/test/parse_test.dart index 613dae4fc..f700b4efa 100644 --- a/pkgs/pubspec_parse/test/parse_test.dart +++ b/pkgs/pubspec_parse/test/parse_test.dart @@ -651,27 +651,21 @@ line 9, column 12: Unsupported value for "path". `42` is not a String group('lenient', () { test('null', () { - expectParseThrows( - null, - r''' + expectParseThrows(null, r''' line 1, column 1: Not a map ╷ 1 │ null │ ^^^^ - ╵''', - lenient: true); + ╵''', lenient: true); }); test('empty string', () { - expectParseThrows( - '', - r''' + expectParseThrows('', r''' line 1, column 1: Not a map ╷ 1 │ "" │ ^^ - ╵''', - lenient: true); + ╵''', lenient: true); }); test('name cannot be empty', () { diff --git a/pkgs/pubspec_parse/test/test_utils.dart b/pkgs/pubspec_parse/test/test_utils.dart index b0b5ef7d2..e04c41088 100644 --- a/pkgs/pubspec_parse/test/test_utils.dart +++ b/pkgs/pubspec_parse/test/test_utils.dart @@ -21,17 +21,17 @@ String _encodeJson(Object? input) => const JsonEncoder.withIndent(' ').convert(input); Matcher _throwsParsedYamlException(String prettyValue) => throwsA( - const TypeMatcher().having( - (e) { - final message = e.formattedMessage; - printOnFailure("Actual error format:\nr'''\n$message'''"); - _printDebugParsedYamlException(e); - return message; - }, - 'formattedMessage', - prettyValue, - ), - ); + const TypeMatcher().having( + (e) { + final message = e.formattedMessage; + printOnFailure("Actual error format:\nr'''\n$message'''"); + _printDebugParsedYamlException(e); + return message; + }, + 'formattedMessage', + prettyValue, + ), +); void _printDebugParsedYamlException(ParsedYamlException e) { var innerError = e.innerError; @@ -113,16 +113,15 @@ void expectParseThrows( String expectedError, { bool skipTryPub = false, bool lenient = false, -}) => - expect( - () => parse( - content, - lenient: lenient, - quietOnError: true, - skipTryPub: skipTryPub, - ), - _throwsParsedYamlException(expectedError), - ); +}) => expect( + () => parse( + content, + lenient: lenient, + quietOnError: true, + skipTryPub: skipTryPub, + ), + _throwsParsedYamlException(expectedError), +); void expectParseThrowsContaining( Object? content, From ca5c509931318afab04250bca9d02a9fc39cad11 Mon Sep 17 00:00:00 2001 From: Dhruv Maradiya Date: Fri, 24 Oct 2025 19:01:29 +0530 Subject: [PATCH 4/5] Add doc comment for Dependency.toJson --- pkgs/pubspec_parse/lib/src/dependency.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/pkgs/pubspec_parse/lib/src/dependency.dart b/pkgs/pubspec_parse/lib/src/dependency.dart index dd1eed371..4a98c9931 100644 --- a/pkgs/pubspec_parse/lib/src/dependency.dart +++ b/pkgs/pubspec_parse/lib/src/dependency.dart @@ -96,6 +96,7 @@ Dependency? _fromJson(Object? data, String name) { } sealed class Dependency { + /// Creates a JSON representation of the data of this object. Map toJson(); } From f9656ff19182ac6d03025d5e3168aaaa6ed77ec7 Mon Sep 17 00:00:00 2001 From: Dhruv Maradiya Date: Fri, 24 Oct 2025 19:35:50 +0530 Subject: [PATCH 5/5] Add blank line separating summary and details in _fromJson doc comment --- pkgs/pubspec_parse/lib/src/dependency.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/pkgs/pubspec_parse/lib/src/dependency.dart b/pkgs/pubspec_parse/lib/src/dependency.dart index 4a98c9931..cf7b98746 100644 --- a/pkgs/pubspec_parse/lib/src/dependency.dart +++ b/pkgs/pubspec_parse/lib/src/dependency.dart @@ -40,6 +40,7 @@ Map parseDeps(Map? source) => const _sourceKeys = ['sdk', 'git', 'path', 'hosted']; /// Converts [data] into a [Dependency] object. +/// /// If [data] is not a valid representation of a dependency, /// returns null so that the parent logic can throw the proper error. Dependency? _fromJson(Object? data, String name) {