From b83a4cafe06ee2af05ad3bef24a16f84fb53cfd1 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Mon, 4 Dec 2017 11:13:58 -0800 Subject: [PATCH 01/88] Package description --- pkgs/graphs/README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 pkgs/graphs/README.md diff --git a/pkgs/graphs/README.md b/pkgs/graphs/README.md new file mode 100644 index 0000000000..5c71acaaeb --- /dev/null +++ b/pkgs/graphs/README.md @@ -0,0 +1,2 @@ +Graph algorithms which do not specify a particular approach for representing a +Graph. From 4d9eaf0ead98e34ba9129c65414410cb7f087947 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Mon, 4 Dec 2017 11:25:39 -0800 Subject: [PATCH 02/88] Dart package boilerplate (#1) --- pkgs/graphs/.gitignore | 5 ++++ pkgs/graphs/CHANGELOG.md | 3 +++ pkgs/graphs/CONTRIBUTING.md | 33 ++++++++++++++++++++++++++ pkgs/graphs/LICENSE | 26 +++++++++++++++++++++ pkgs/graphs/analysis_options.yaml | 39 +++++++++++++++++++++++++++++++ pkgs/graphs/pubspec.yaml | 13 +++++++++++ 6 files changed, 119 insertions(+) create mode 100644 pkgs/graphs/.gitignore create mode 100644 pkgs/graphs/CHANGELOG.md create mode 100644 pkgs/graphs/CONTRIBUTING.md create mode 100644 pkgs/graphs/LICENSE create mode 100644 pkgs/graphs/analysis_options.yaml create mode 100644 pkgs/graphs/pubspec.yaml diff --git a/pkgs/graphs/.gitignore b/pkgs/graphs/.gitignore new file mode 100644 index 0000000000..ddfdca160f --- /dev/null +++ b/pkgs/graphs/.gitignore @@ -0,0 +1,5 @@ +.dart_tool/ +.packages +.pub/ +build/ +pubspec.lock diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md new file mode 100644 index 0000000000..ca6f2a9492 --- /dev/null +++ b/pkgs/graphs/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.1.0-dev + +- Initial release diff --git a/pkgs/graphs/CONTRIBUTING.md b/pkgs/graphs/CONTRIBUTING.md new file mode 100644 index 0000000000..286d61c8bc --- /dev/null +++ b/pkgs/graphs/CONTRIBUTING.md @@ -0,0 +1,33 @@ +Want to contribute? Great! First, read this page (including the small print at +the end). + +### Before you contribute +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. + +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +### Code reviews +All submissions, including submissions by project members, require review. + +### File headers +All files in the project must start with the following header. + + // Copyright (c) 2017, 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. + +### The small print +Contributions made by corporations are covered by a different agreement than the +one above, the +[Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). diff --git a/pkgs/graphs/LICENSE b/pkgs/graphs/LICENSE new file mode 100644 index 0000000000..389ce98563 --- /dev/null +++ b/pkgs/graphs/LICENSE @@ -0,0 +1,26 @@ +Copyright 2017, the Dart project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkgs/graphs/analysis_options.yaml b/pkgs/graphs/analysis_options.yaml new file mode 100644 index 0000000000..125415f3ee --- /dev/null +++ b/pkgs/graphs/analysis_options.yaml @@ -0,0 +1,39 @@ +analyzer: + strong-mode: + implicit-casts: false + errors: + unused_import: error + unused_local_variable: error + dead_code: error + override_on_non_overriding_method: error +linter: + rules: + # Errors + - avoid_empty_else + - await_only_futures + - comment_references + - control_flow_in_finally + - empty_statements + - hash_and_equals + - test_types_in_equals + - throw_in_finally + - unawaited_futures + - unrelated_type_equality_checks + - valid_regexps + + # Style + - annotate_overrides + - avoid_init_to_null + - avoid_return_types_on_setters + - camel_case_types + - directives_ordering + - empty_catches + - empty_constructor_bodies + - library_names + - library_prefixes + - non_constant_identifier_names + - prefer_typing_uninitialized_variables + - prefer_final_fields + - prefer_is_not_empty + - slash_for_doc_comments + - type_init_formals diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml new file mode 100644 index 0000000000..b7e88441cb --- /dev/null +++ b/pkgs/graphs/pubspec.yaml @@ -0,0 +1,13 @@ +name: graphs +version: 0.1.0-dev +description: Graph algorithms operation an graphs in any representation. +author: Dart Team +homepage: https://github.com/dart-lang/graphs + +environment: + sdk: '>=1.24.0 <2.0.0' + +dependencies: + +dev_dependencies: + test: ^0.12.0 From d911c4767cfca5a2fe68977df0a4eaa678deb1c7 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Mon, 4 Dec 2017 13:45:53 -0800 Subject: [PATCH 03/88] Implement stronglyConnectedComponents (#2) --- pkgs/graphs/CHANGELOG.md | 2 +- pkgs/graphs/lib/graphs.dart | 2 + .../src/strongly_connected_components.dart | 69 ++++++++++ .../strongly_connected_components_test.dart | 119 ++++++++++++++++++ pkgs/graphs/test/utils/graph.dart | 10 ++ 5 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 pkgs/graphs/lib/graphs.dart create mode 100644 pkgs/graphs/lib/src/strongly_connected_components.dart create mode 100644 pkgs/graphs/test/strongly_connected_components_test.dart create mode 100644 pkgs/graphs/test/utils/graph.dart diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index ca6f2a9492..e52b25e003 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,3 +1,3 @@ # 0.1.0-dev -- Initial release +- Initial release with an implementation of `stronglyConnectedComponents`. diff --git a/pkgs/graphs/lib/graphs.dart b/pkgs/graphs/lib/graphs.dart new file mode 100644 index 0000000000..93bba4ccee --- /dev/null +++ b/pkgs/graphs/lib/graphs.dart @@ -0,0 +1,2 @@ +export 'src/strongly_connected_components.dart' + show stronglyConnectedComponents; diff --git a/pkgs/graphs/lib/src/strongly_connected_components.dart b/pkgs/graphs/lib/src/strongly_connected_components.dart new file mode 100644 index 0000000000..342b23c4dd --- /dev/null +++ b/pkgs/graphs/lib/src/strongly_connected_components.dart @@ -0,0 +1,69 @@ +// Copyright (c) 2017, 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. + +import 'dart:collection'; +import 'dart:math' show min; + +/// Finds the strongly connected components of an ordered graph using Tarjan's +/// algorithm. +/// +/// The result will be a valid topological order ordering of the strongly +/// connected components. Elements further from a root will appear in the +/// result before the components which they are children of. +/// +/// Nodes within a strongly connected component have no ordering guarantees, +/// except that if the first value in [nodes] is a valid root, and is contained +/// in a cycle, it will be the last element of that cycle. +/// +/// [V] is the type of values in the graph nodes. [K] must be a type suitable +/// for using as a Map or Set key, and [key] must provide a consistent key for +/// every node. [children] should return the next reachable nodes. +/// +/// [nodes] must contain at least a root of every tree in the graph if there are +/// disjoint subgraphs but it may contain all nodes in the graph if the roots +/// are not known. +List> stronglyConnectedComponents( + Iterable nodes, K Function(V) key, Iterable Function(V) children) { + final result = >[]; + final lowLinks = {}; + final indexes = {}; + final onStack = new Set(); + + var index = 0; + var lastVisited = new Queue(); + + void strongConnect(V node) { + var nodeKey = key(node); + indexes[nodeKey] = index; + lowLinks[nodeKey] = index; + index++; + lastVisited.addLast(node); + onStack.add(nodeKey); + for (final next in children(node)) { + var nextKey = key(next); + if (!indexes.containsKey(nextKey)) { + strongConnect(next); + lowLinks[nodeKey] = min(lowLinks[nodeKey], lowLinks[nextKey]); + } else if (onStack.contains(nextKey)) { + lowLinks[nodeKey] = min(lowLinks[nodeKey], indexes[nextKey]); + } + } + if (lowLinks[nodeKey] == indexes[nodeKey]) { + final component = []; + K nextKey; + do { + var next = lastVisited.removeLast(); + nextKey = key(next); + onStack.remove(nextKey); + component.add(next); + } while (nextKey != nodeKey); + result.add(component); + } + } + + for (final node in nodes) { + if (!indexes.containsKey(key(node))) strongConnect(node); + } + return result; +} diff --git a/pkgs/graphs/test/strongly_connected_components_test.dart b/pkgs/graphs/test/strongly_connected_components_test.dart new file mode 100644 index 0000000000..48b95eb952 --- /dev/null +++ b/pkgs/graphs/test/strongly_connected_components_test.dart @@ -0,0 +1,119 @@ +import 'package:test/test.dart'; +import 'package:graphs/graphs.dart'; + +import 'utils/graph.dart'; + +void main() { + group('strongly connected components', () { + /// Run [stronglyConnectedComponents] on [g]. + List> components(Map> g) { + final graph = new Graph(g); + return stronglyConnectedComponents( + graph.allNodes, graph.key, graph.children); + } + + test('empty result for empty graph', () { + var result = components({}); + expect(result, isEmpty); + }); + + test('single item for single node', () { + var result = components({'a': []}); + expect(result, [ + ['a'] + ]); + }); + + test('handles non-cycles', () { + var result = components({ + 'a': ['b'], + 'b': ['c'], + 'c': [] + }); + expect(result, [ + ['c'], + ['b'], + ['a'] + ]); + }); + + test('handles entire graph as cycle', () { + var result = components({ + 'a': ['b'], + 'b': ['c'], + 'c': ['a'] + }); + expect(result, [allOf(contains('a'), contains('b'), contains('c'))]); + }); + + test('includes the first passed root last in a cycle', () { + // In cases where this is used to find a topological ordering the first + // value in nodes should always come last. + var graph = new Graph({ + 'a': ['b'], + 'b': ['a'] + }); + var resultFromA = + stronglyConnectedComponents(['a'], graph.key, graph.children); + var resultFromB = + stronglyConnectedComponents(['b'], graph.key, graph.children); + expect(resultFromA.single.last, 'a'); + expect(resultFromB.single.last, 'b'); + }); + + test('handles cycles in the middle', () { + var result = components({ + 'a': ['b', 'c'], + 'b': ['c', 'd'], + 'c': ['b', 'd'], + 'd': [], + }); + expect(result, [ + ['d'], + allOf(contains('b'), contains('c')), + ['a'], + ]); + }); + + test('valid topological ordering for disjoint subgraphs', () { + var result = components({ + 'a': ['b', 'c'], + 'b': ['b1', 'b2'], + 'c': ['c1', 'c2'], + 'b1': [], + 'b2': [], + 'c1': [], + 'c2': [] + }); + + expect( + result, + containsAllInOrder([ + ['c1'], + ['c'], + ['a'] + ])); + expect( + result, + containsAllInOrder([ + ['c2'], + ['c'], + ['a'] + ])); + expect( + result, + containsAllInOrder([ + ['b1'], + ['b'], + ['a'] + ])); + expect( + result, + containsAllInOrder([ + ['b2'], + ['b'], + ['a'] + ])); + }); + }); +} diff --git a/pkgs/graphs/test/utils/graph.dart b/pkgs/graphs/test/utils/graph.dart new file mode 100644 index 0000000000..e617cf7dc7 --- /dev/null +++ b/pkgs/graphs/test/utils/graph.dart @@ -0,0 +1,10 @@ +/// A representation of a Graph since none is specified in `lib/`. +class Graph { + final Map> graph; + + Graph(this.graph); + + String key(String node) => node; + List children(String node) => graph[node]; + Iterable get allNodes => graph.keys; +} From b9dfcba007d2cb8c8d59b755d5eda16f521e7b43 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Mon, 4 Dec 2017 14:03:52 -0800 Subject: [PATCH 04/88] Add Travis config (#3) --- pkgs/graphs/.travis.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 pkgs/graphs/.travis.yml diff --git a/pkgs/graphs/.travis.yml b/pkgs/graphs/.travis.yml new file mode 100644 index 0000000000..e24ffbb22a --- /dev/null +++ b/pkgs/graphs/.travis.yml @@ -0,0 +1,20 @@ +language: dart +dart: + - stable + - dev + +dart_task: + - test + - test -p chrome + - test -p firefox + - dartfmt + - dartanalyzer + +matrix: + exclude: + - dart: stable + dart_task: dartfmt + +cache: + directories: + - $HOME/.pub-cache From d696165507f7b42bc405920a9017eb3fcb186726 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Mon, 4 Dec 2017 14:09:32 -0800 Subject: [PATCH 05/88] Only test on master branch (#4) --- pkgs/graphs/.travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkgs/graphs/.travis.yml b/pkgs/graphs/.travis.yml index e24ffbb22a..fbdbe35148 100644 --- a/pkgs/graphs/.travis.yml +++ b/pkgs/graphs/.travis.yml @@ -15,6 +15,9 @@ matrix: - dart: stable dart_task: dartfmt +branches: + only: [master] + cache: directories: - $HOME/.pub-cache From f75feb60de048596f0d3e293633317373e1df3c1 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Mon, 4 Dec 2017 15:10:26 -0800 Subject: [PATCH 06/88] Add test for self-cycles (#6) --- .../test/strongly_connected_components_test.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkgs/graphs/test/strongly_connected_components_test.dart b/pkgs/graphs/test/strongly_connected_components_test.dart index 48b95eb952..b696ad59c4 100644 --- a/pkgs/graphs/test/strongly_connected_components_test.dart +++ b/pkgs/graphs/test/strongly_connected_components_test.dart @@ -75,6 +75,17 @@ void main() { ]); }); + test('handles self cycles', () { + var result = components({ + 'a': ['b'], + 'b': ['b'], + }); + expect(result, [ + ['b'], + ['a'], + ]); + }); + test('valid topological ordering for disjoint subgraphs', () { var result = components({ 'a': ['b', 'c'], From edd95720c5bc2e706adf3ffbbb300f08393029fc Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Mon, 4 Dec 2017 15:11:28 -0800 Subject: [PATCH 07/88] Expand README and add example (#5) --- pkgs/graphs/README.md | 39 +++++++++++++++++++++++++++++ pkgs/graphs/example/example.dart | 42 ++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 pkgs/graphs/example/example.dart diff --git a/pkgs/graphs/README.md b/pkgs/graphs/README.md index 5c71acaaeb..e761d273d2 100644 --- a/pkgs/graphs/README.md +++ b/pkgs/graphs/README.md @@ -1,2 +1,41 @@ +# [![Build Status](https://travis-ci.org/dart-lang/graphs.svg?branch=master)](https://travis-ci.org/dart-lang/graphs) + Graph algorithms which do not specify a particular approach for representing a Graph. + +Functions in this package will take arguments that provide the mechanism for +traversing the graph. For example two common approaches for representing a +graph: + +```dart +class Graph { + Map> nodes; +} +class Node { + // Interesting data +} +``` + +```dart +class Graph { + Node root; +} +class Node { + List children; + // Interesting data +} +``` + +Any representation can be adapted to the needs of the algorithm: + +- Some algorithms need to associate data with each node in the graph and it will + be keyed by some type `K` that must work as a key in a `HashMap`. If nodes + implement `hashCode` and `==`, or if they are known to have one instance per + logical node such that instance equality is sufficient, then the node can be + passed through directly. + - `(node) => node` + - `(node) => node.id` +- Algorithms which need to traverse the graph take a `children` function which + provides the reachable nodes. + - `(node) => graph[node]` + - `(node) => node.children` diff --git a/pkgs/graphs/example/example.dart b/pkgs/graphs/example/example.dart new file mode 100644 index 0000000000..087a1198f0 --- /dev/null +++ b/pkgs/graphs/example/example.dart @@ -0,0 +1,42 @@ +import 'package:graphs/graphs.dart'; + +/// A representation of a directed graph. +/// +/// Data is stored on the [Node] class. +class Graph { + final Map> nodes; + Graph(this.nodes); +} + +class Node { + final String id; + final int data; + + Node(this.id, this.data); + + @override + bool operator ==(Object other) => other is Node && other.id == id; + + @override + int get hashCode => id.hashCode; + + @override + String toString() => '<$id -> $data>'; +} + +void main() { + var nodeA = new Node('A', 1); + var nodeB = new Node('B', 2); + var nodeC = new Node('C', 3); + var nodeD = new Node('D', 4); + var graph = new Graph({ + nodeA: [nodeB, nodeC], + nodeB: [nodeC, nodeD], + nodeC: [nodeB, nodeD] + }); + + var components = stronglyConnectedComponents( + graph.nodes.keys, (node) => node, (node) => graph.nodes[node] ?? []); + + print(components); +} From 7ea7bc4b7d09208fbfa040c0108838b4c0508014 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Mon, 4 Dec 2017 15:18:34 -0800 Subject: [PATCH 08/88] Allow `children` to return null (#8) --- pkgs/graphs/example/example.dart | 2 +- .../graphs/lib/src/strongly_connected_components.dart | 2 +- .../test/strongly_connected_components_test.dart | 11 +++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/example/example.dart b/pkgs/graphs/example/example.dart index 087a1198f0..09298a6b6e 100644 --- a/pkgs/graphs/example/example.dart +++ b/pkgs/graphs/example/example.dart @@ -36,7 +36,7 @@ void main() { }); var components = stronglyConnectedComponents( - graph.nodes.keys, (node) => node, (node) => graph.nodes[node] ?? []); + graph.nodes.keys, (node) => node, (node) => graph.nodes[node]); print(components); } diff --git a/pkgs/graphs/lib/src/strongly_connected_components.dart b/pkgs/graphs/lib/src/strongly_connected_components.dart index 342b23c4dd..18cae4308c 100644 --- a/pkgs/graphs/lib/src/strongly_connected_components.dart +++ b/pkgs/graphs/lib/src/strongly_connected_components.dart @@ -40,7 +40,7 @@ List> stronglyConnectedComponents( index++; lastVisited.addLast(node); onStack.add(nodeKey); - for (final next in children(node)) { + for (final next in children(node) ?? const []) { var nextKey = key(next); if (!indexes.containsKey(nextKey)) { strongConnect(next); diff --git a/pkgs/graphs/test/strongly_connected_components_test.dart b/pkgs/graphs/test/strongly_connected_components_test.dart index b696ad59c4..d8a64f3bd3 100644 --- a/pkgs/graphs/test/strongly_connected_components_test.dart +++ b/pkgs/graphs/test/strongly_connected_components_test.dart @@ -126,5 +126,16 @@ void main() { ['a'] ])); }); + + test('handles getting null for children', () { + var result = components({ + 'a': ['b'], + 'b': null, + }); + expect(result, [ + ['b'], + ['a'] + ]); + }); }); } From 5f4c330eb4113c81a4612c91042cd94ab34a8eba Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Tue, 5 Dec 2017 08:58:46 -0800 Subject: [PATCH 09/88] Use more precise language in docs for SCC (#9) While implementing crawlAsync I noticed some discrepancies: - Use 'directed' rather than 'ordered' graph - This is a reverse topological sort, not a topolocical sort --- pkgs/graphs/lib/src/strongly_connected_components.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkgs/graphs/lib/src/strongly_connected_components.dart b/pkgs/graphs/lib/src/strongly_connected_components.dart index 18cae4308c..eacc860865 100644 --- a/pkgs/graphs/lib/src/strongly_connected_components.dart +++ b/pkgs/graphs/lib/src/strongly_connected_components.dart @@ -5,12 +5,12 @@ import 'dart:collection'; import 'dart:math' show min; -/// Finds the strongly connected components of an ordered graph using Tarjan's +/// Finds the strongly connected components of a directed graph using Tarjan's /// algorithm. /// -/// The result will be a valid topological order ordering of the strongly -/// connected components. Elements further from a root will appear in the -/// result before the components which they are children of. +/// The result will be a valid reverse topological order ordering of the +/// strongly connected components. Components further from a root will appear in +/// the result before the components which they are children of. /// /// Nodes within a strongly connected component have no ordering guarantees, /// except that if the first value in [nodes] is a valid root, and is contained From 3ba4fcc01f835d322ba9cc01649db93df3e844df Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Tue, 5 Dec 2017 11:28:16 -0800 Subject: [PATCH 10/88] Implement crawlAsync (#10) Implemented with no ordering guarantees since this is the simplest and likely most performance. Add an example crawing transitive imports since this matches our known use case. --- pkgs/graphs/README.md | 3 + pkgs/graphs/example/crawl_async_example.dart | 48 ++++++++++++ pkgs/graphs/lib/graphs.dart | 1 + pkgs/graphs/lib/src/crawl_async.dart | 67 +++++++++++++++++ pkgs/graphs/pubspec.yaml | 4 + pkgs/graphs/test/crawl_async_test.dart | 78 ++++++++++++++++++++ pkgs/graphs/test/utils/graph.dart | 14 ++++ 7 files changed, 215 insertions(+) create mode 100644 pkgs/graphs/example/crawl_async_example.dart create mode 100644 pkgs/graphs/lib/src/crawl_async.dart create mode 100644 pkgs/graphs/test/crawl_async_test.dart diff --git a/pkgs/graphs/README.md b/pkgs/graphs/README.md index e761d273d2..c54e615a5e 100644 --- a/pkgs/graphs/README.md +++ b/pkgs/graphs/README.md @@ -39,3 +39,6 @@ Any representation can be adapted to the needs of the algorithm: provides the reachable nodes. - `(node) => graph[node]` - `(node) => node.children` + +Graphs which are resolved asynchronously will have similar functions which +return `FutureOr`. diff --git a/pkgs/graphs/example/crawl_async_example.dart b/pkgs/graphs/example/crawl_async_example.dart new file mode 100644 index 0000000000..b9d91e3c58 --- /dev/null +++ b/pkgs/graphs/example/crawl_async_example.dart @@ -0,0 +1,48 @@ +// Copyright (c) 2017, 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. + +import 'dart:async'; + +import 'package:analyzer/analyzer.dart' show parseDirectives; +import 'package:analyzer/dart/ast/ast.dart' show UriBasedDirective; +import 'package:resource/resource.dart'; +import 'package:graphs/graphs.dart'; +import 'package:path/path.dart' as p; + +class Source { + final Uri uri; + final String content; + + Source(this.uri, this.content); +} + +Future read(Uri uri) async => + new Source(uri, await new Resource(uri).readAsString()); + +Iterable findImports(Uri from, Source source) { + final unit = parseDirectives(source.content); + return unit.directives + .where((d) => d is UriBasedDirective) + .map((d) => (d as UriBasedDirective).uri.stringValue) + .where((uri) => !uri.startsWith('dart:')) + .map((import) => resolveImport(import, from)); +} + +Uri resolveImport(String import, Uri from) { + if (import.startsWith('package:')) return Uri.parse(import); + assert(from.scheme == 'package'); + final package = from.pathSegments.first; + final fromPath = p.joinAll(from.pathSegments.skip(1)); + final path = p.normalize(p.join(p.dirname(fromPath), import)); + return Uri.parse('package:${p.join(package, path)}'); +} + +/// Print a transitive set of imported URIs where libraries are read +/// asynchronously. +Future main() async { + var allImports = await crawlAsync( + [Uri.parse('package:graphs/graphs.dart')], read, findImports) + .toList(); + print(allImports.map((s) => s.uri).toList()); +} diff --git a/pkgs/graphs/lib/graphs.dart b/pkgs/graphs/lib/graphs.dart index 93bba4ccee..661245ef42 100644 --- a/pkgs/graphs/lib/graphs.dart +++ b/pkgs/graphs/lib/graphs.dart @@ -1,2 +1,3 @@ +export 'src/crawl_async.dart' show crawlAsync; export 'src/strongly_connected_components.dart' show stronglyConnectedComponents; diff --git a/pkgs/graphs/lib/src/crawl_async.dart b/pkgs/graphs/lib/src/crawl_async.dart new file mode 100644 index 0000000000..50a26a750c --- /dev/null +++ b/pkgs/graphs/lib/src/crawl_async.dart @@ -0,0 +1,67 @@ +// Copyright (c) 2017, 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. + +import 'dart:async'; + +final _empty = new Future.value(null); + +/// Finds and returns every node in a graph who's nodes and edges are +/// asynchronously resolved. +/// +/// Cycles are allowed. If this is an undirected graph the [children] function +/// may be symmetric. In this case the [roots] may be any node in each connected +/// graph. +/// +/// [V] is the type of values in the graph nodes. [K] must be a type suitable +/// for using as a Map or Set key. [children] should return the next reachable +/// nodes. +/// +/// There are no ordering guarantees. This is useful for ensuring some work is +/// performed at every node in an asynchronous graph, but does not give +/// guarantees that the work is done in topological order. +Stream crawlAsync(Iterable roots, FutureOr Function(K) readNode, + FutureOr> Function(K, V) children) { + final crawl = new _CrawlAsync(roots, readNode, children)..run(); + return crawl.result.stream; +} + +class _CrawlAsync { + final result = new StreamController(); + + final FutureOr Function(K) readNode; + final FutureOr> Function(K, V) children; + final Iterable roots; + + final _seen = new Set(); + + _CrawlAsync(this.roots, this.readNode, this.children); + + /// Add all nodes in the graph to [result] and return a Future which fires + /// after all nodes have been seen. + Future run() async { + await Future.wait(roots.map(_visit)); + await result.close(); + } + + /// Resolve the node at [key] and output it, then start crawling all of it's + /// children. + Future _crawlFrom(K key) async { + var value = await readNode(key); + result.add(value); + var next = await children(key, value) ?? const []; + await Future.wait(next.map(_visit)); + } + + /// Synchronously record that [key] is being handled then start work on the + /// node for [key]. + /// + /// The returned Future will complete only after the work for [key] and all + /// transitively reachable nodes has either been finished, or will be finished + /// by some other Future in [_seen]. + Future _visit(K key) { + if (_seen.contains(key)) return _empty; + _seen.add(key); + return _crawlFrom(key); + } +} diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index b7e88441cb..978cd8dd2b 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -11,3 +11,7 @@ dependencies: dev_dependencies: test: ^0.12.0 + # For examples + analyzer: ^0.30.0 + path: ^1.1.0 + resource: ^2.1.0 diff --git a/pkgs/graphs/test/crawl_async_test.dart b/pkgs/graphs/test/crawl_async_test.dart new file mode 100644 index 0000000000..7530a1a563 --- /dev/null +++ b/pkgs/graphs/test/crawl_async_test.dart @@ -0,0 +1,78 @@ +// Copyright (c) 2017, 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. + +import 'dart:async'; + +import 'package:test/test.dart'; + +import 'package:graphs/graphs.dart'; + +import 'utils/graph.dart'; + +void main() { + group('asyncCrawl', () { + Future> crawl( + Map> g, Iterable roots) { + var graph = new AsyncGraph(g); + return crawlAsync(roots, graph.readNode, graph.children).toList(); + } + + test('empty result for empty graph', () async { + var result = await crawl({}, []); + expect(result, isEmpty); + }); + + test('single item for a single node', () async { + var result = await crawl({'a': []}, ['a']); + expect(result, ['a']); + }); + + test('hits every node in a graph', () async { + var result = await crawl({ + 'a': ['b', 'c'], + 'b': ['c'], + 'c': ['d'], + }, [ + 'a' + ]); + expect(result, hasLength(4)); + expect(result, + allOf(contains('a'), contains('b'), contains('c'), contains('d'))); + }); + + test('handles cycles', () async { + var result = await crawl({ + 'a': ['b'], + 'b': ['c'], + 'c': ['b'], + }, [ + 'a' + ]); + expect(result, hasLength(3)); + expect(result, allOf(contains('a'), contains('b'), contains('c'))); + }); + + test('handles self cycles', () async { + var result = await crawl({ + 'a': ['b'], + 'b': ['b'], + }, [ + 'a' + ]); + expect(result, hasLength(2)); + expect(result, allOf(contains('a'), contains('b'))); + }); + + test('allows null children', () async { + var result = await crawl({ + 'a': ['b'], + 'b': null, + }, [ + 'a' + ]); + expect(result, hasLength(2)); + expect(result, allOf(contains('a'), contains('b'))); + }); + }); +} diff --git a/pkgs/graphs/test/utils/graph.dart b/pkgs/graphs/test/utils/graph.dart index e617cf7dc7..4c667d47b4 100644 --- a/pkgs/graphs/test/utils/graph.dart +++ b/pkgs/graphs/test/utils/graph.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + /// A representation of a Graph since none is specified in `lib/`. class Graph { final Map> graph; @@ -8,3 +10,15 @@ class Graph { List children(String node) => graph[node]; Iterable get allNodes => graph.keys; } + +/// A representation of a Graph where keys can asynchronously be resolved to +/// real values or to children. +class AsyncGraph { + final Map> graph; + + AsyncGraph(this.graph); + + Future readNode(String node) async => node; + Future> children(String key, String node) async => + graph[key]; +} From d3b1290583853da26983052f5ed6a3681a226ec7 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Tue, 5 Dec 2017 12:57:47 -0800 Subject: [PATCH 11/88] Add missing copyright notices, update changelog (#11) --- pkgs/graphs/CHANGELOG.md | 3 ++- pkgs/graphs/example/example.dart | 4 ++++ pkgs/graphs/lib/graphs.dart | 4 ++++ pkgs/graphs/test/strongly_connected_components_test.dart | 4 ++++ pkgs/graphs/test/utils/graph.dart | 4 ++++ 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index e52b25e003..62de6965da 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,3 +1,4 @@ # 0.1.0-dev -- Initial release with an implementation of `stronglyConnectedComponents`. +- Initial release with an implementation of `stronglyConnectedComponents` and + `crawlAsync`. diff --git a/pkgs/graphs/example/example.dart b/pkgs/graphs/example/example.dart index 09298a6b6e..ce37128467 100644 --- a/pkgs/graphs/example/example.dart +++ b/pkgs/graphs/example/example.dart @@ -1,3 +1,7 @@ +// Copyright (c) 2017, 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. + import 'package:graphs/graphs.dart'; /// A representation of a directed graph. diff --git a/pkgs/graphs/lib/graphs.dart b/pkgs/graphs/lib/graphs.dart index 661245ef42..8f55504bab 100644 --- a/pkgs/graphs/lib/graphs.dart +++ b/pkgs/graphs/lib/graphs.dart @@ -1,3 +1,7 @@ +// Copyright (c) 2017, 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. + export 'src/crawl_async.dart' show crawlAsync; export 'src/strongly_connected_components.dart' show stronglyConnectedComponents; diff --git a/pkgs/graphs/test/strongly_connected_components_test.dart b/pkgs/graphs/test/strongly_connected_components_test.dart index d8a64f3bd3..34d8355fbc 100644 --- a/pkgs/graphs/test/strongly_connected_components_test.dart +++ b/pkgs/graphs/test/strongly_connected_components_test.dart @@ -1,3 +1,7 @@ +// Copyright (c) 2017, 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. + import 'package:test/test.dart'; import 'package:graphs/graphs.dart'; diff --git a/pkgs/graphs/test/utils/graph.dart b/pkgs/graphs/test/utils/graph.dart index 4c667d47b4..66dae13a09 100644 --- a/pkgs/graphs/test/utils/graph.dart +++ b/pkgs/graphs/test/utils/graph.dart @@ -1,3 +1,7 @@ +// Copyright (c) 2017, 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. + import 'dart:async'; /// A representation of a Graph since none is specified in `lib/`. From 5d98ea2983383197fddf487a9cfaaeba95295135 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Tue, 5 Dec 2017 13:11:52 -0800 Subject: [PATCH 12/88] Add more lints (#12) --- pkgs/graphs/analysis_options.yaml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pkgs/graphs/analysis_options.yaml b/pkgs/graphs/analysis_options.yaml index 125415f3ee..a6125b9bf1 100644 --- a/pkgs/graphs/analysis_options.yaml +++ b/pkgs/graphs/analysis_options.yaml @@ -15,16 +15,22 @@ linter: - control_flow_in_finally - empty_statements - hash_and_equals + - iterable_contains_unrelated_type + - no_duplicate_case_values - test_types_in_equals - throw_in_finally - unawaited_futures + - unnecessary_statements - unrelated_type_equality_checks - valid_regexps # Style - annotate_overrides + - avoid_function_literals_in_foreach_calls - avoid_init_to_null - avoid_return_types_on_setters + - avoid_returning_null + - avoid_unused_constructor_parameters - camel_case_types - directives_ordering - empty_catches @@ -32,8 +38,15 @@ linter: - library_names - library_prefixes - non_constant_identifier_names - - prefer_typing_uninitialized_variables + - prefer_conditional_assignment - prefer_final_fields + - prefer_is_empty - prefer_is_not_empty + - prefer_typing_uninitialized_variables + - recursive_getters - slash_for_doc_comments - type_init_formals + - unnecessary_brace_in_string_interps + - unnecessary_getter_setters + - unnecessary_lambda + - unnecessary_null_aware_assignments From b16ce0cc9def501a299b9b4009b339628c21ea47 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Thu, 7 Dec 2017 13:54:05 -0800 Subject: [PATCH 13/88] Prepare to publish 0.1.0 (#13) --- pkgs/graphs/CHANGELOG.md | 2 +- pkgs/graphs/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index 62de6965da..499f0be809 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,4 +1,4 @@ -# 0.1.0-dev +# 0.1.0 - Initial release with an implementation of `stronglyConnectedComponents` and `crawlAsync`. diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 978cd8dd2b..c581f53ffc 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,5 +1,5 @@ name: graphs -version: 0.1.0-dev +version: 0.1.0 description: Graph algorithms operation an graphs in any representation. author: Dart Team homepage: https://github.com/dart-lang/graphs From 5876b0b7af972e859945bbf3dd271992491d9c15 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Wed, 17 Jan 2018 09:28:14 -0800 Subject: [PATCH 14/88] Add explicit type to for-in loop (#16) Workaround for https://github.com/dart-lang/sdk/issues/31884 in the dev.17.0 version of the SDK. --- pkgs/graphs/lib/src/strongly_connected_components.dart | 2 +- pkgs/graphs/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/lib/src/strongly_connected_components.dart b/pkgs/graphs/lib/src/strongly_connected_components.dart index eacc860865..a68e25afb7 100644 --- a/pkgs/graphs/lib/src/strongly_connected_components.dart +++ b/pkgs/graphs/lib/src/strongly_connected_components.dart @@ -40,7 +40,7 @@ List> stronglyConnectedComponents( index++; lastVisited.addLast(node); onStack.add(nodeKey); - for (final next in children(node) ?? const []) { + for (final V next in children(node) ?? const []) { var nextKey = key(next); if (!indexes.containsKey(nextKey)) { strongConnect(next); diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index c581f53ffc..e80472ec04 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,5 +1,5 @@ name: graphs -version: 0.1.0 +version: 0.1.1-dev description: Graph algorithms operation an graphs in any representation. author: Dart Team homepage: https://github.com/dart-lang/graphs From 7e5a23c377a71e2e183c05c262b835171ebb4b87 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Wed, 18 Apr 2018 15:35:21 -0700 Subject: [PATCH 15/88] Ignore null nodes in `crawlAsync` (#17) --- pkgs/graphs/CHANGELOG.md | 4 ++++ pkgs/graphs/lib/src/crawl_async.dart | 5 +++++ pkgs/graphs/pubspec.yaml | 2 +- pkgs/graphs/test/crawl_async_test.dart | 10 ++++++++++ pkgs/graphs/test/utils/graph.dart | 3 ++- 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index 499f0be809..87b4252f99 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.1.1 + +- `crawlAsync` will now ignore nodes that are resolved to `null`. + # 0.1.0 - Initial release with an implementation of `stronglyConnectedComponents` and diff --git a/pkgs/graphs/lib/src/crawl_async.dart b/pkgs/graphs/lib/src/crawl_async.dart index 50a26a750c..7b579f2b5d 100644 --- a/pkgs/graphs/lib/src/crawl_async.dart +++ b/pkgs/graphs/lib/src/crawl_async.dart @@ -20,6 +20,10 @@ final _empty = new Future.value(null); /// There are no ordering guarantees. This is useful for ensuring some work is /// performed at every node in an asynchronous graph, but does not give /// guarantees that the work is done in topological order. +/// +/// If [readNode] returns null for any key it will be ignored from the rest of +/// the graph. If missing nodes are important they should be tracked within the +/// [readNode] callback. Stream crawlAsync(Iterable roots, FutureOr Function(K) readNode, FutureOr> Function(K, V) children) { final crawl = new _CrawlAsync(roots, readNode, children)..run(); @@ -48,6 +52,7 @@ class _CrawlAsync { /// children. Future _crawlFrom(K key) async { var value = await readNode(key); + if (value == null) return; result.add(value); var next = await children(key, value) ?? const []; await Future.wait(next.map(_visit)); diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index e80472ec04..e57eebdc16 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,5 +1,5 @@ name: graphs -version: 0.1.1-dev +version: 0.1.1 description: Graph algorithms operation an graphs in any representation. author: Dart Team homepage: https://github.com/dart-lang/graphs diff --git a/pkgs/graphs/test/crawl_async_test.dart b/pkgs/graphs/test/crawl_async_test.dart index 7530a1a563..a4a3ff2324 100644 --- a/pkgs/graphs/test/crawl_async_test.dart +++ b/pkgs/graphs/test/crawl_async_test.dart @@ -33,6 +33,7 @@ void main() { 'a': ['b', 'c'], 'b': ['c'], 'c': ['d'], + 'd': [], }, [ 'a' ]); @@ -74,5 +75,14 @@ void main() { expect(result, hasLength(2)); expect(result, allOf(contains('a'), contains('b'))); }); + + test('ignores null nodes', () async { + var result = await crawl({ + 'a': ['b'], + }, [ + 'a' + ]); + expect(result, ['a']); + }); }); } diff --git a/pkgs/graphs/test/utils/graph.dart b/pkgs/graphs/test/utils/graph.dart index 66dae13a09..a4ff65c59b 100644 --- a/pkgs/graphs/test/utils/graph.dart +++ b/pkgs/graphs/test/utils/graph.dart @@ -22,7 +22,8 @@ class AsyncGraph { AsyncGraph(this.graph); - Future readNode(String node) async => node; + Future readNode(String node) async => + graph.containsKey(node) ? node : null; Future> children(String key, String node) async => graph[key]; } From 1f3e91b9298e223dcc50a6fdd7bef86fce287b07 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Mon, 23 Apr 2018 15:52:04 -0700 Subject: [PATCH 16/88] Surface crawlAsync errors through the Stream (#18) Also use `eagerError` and check for alrady closed streams to prune work when there are errors. --- pkgs/graphs/CHANGELOG.md | 5 +++++ pkgs/graphs/lib/src/crawl_async.dart | 16 +++++++++++++--- pkgs/graphs/pubspec.yaml | 2 +- pkgs/graphs/test/crawl_async_test.dart | 18 ++++++++++++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index 87b4252f99..6fcff6a639 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,3 +1,8 @@ +# 0.1.2 + +- `crawlAsync` surfaces exceptions while crawling through the result stream + rather than as uncaught asynchronous errors. + # 0.1.1 - `crawlAsync` will now ignore nodes that are resolved to `null`. diff --git a/pkgs/graphs/lib/src/crawl_async.dart b/pkgs/graphs/lib/src/crawl_async.dart index 7b579f2b5d..243cc99fd5 100644 --- a/pkgs/graphs/lib/src/crawl_async.dart +++ b/pkgs/graphs/lib/src/crawl_async.dart @@ -24,6 +24,10 @@ final _empty = new Future.value(null); /// If [readNode] returns null for any key it will be ignored from the rest of /// the graph. If missing nodes are important they should be tracked within the /// [readNode] callback. +/// +/// If either [readNode] or [children] throws the error will be forwarded +/// through the result stream and no further nodes will be crawled, though some +/// work may have already been started. Stream crawlAsync(Iterable roots, FutureOr Function(K) readNode, FutureOr> Function(K, V) children) { final crawl = new _CrawlAsync(roots, readNode, children)..run(); @@ -44,8 +48,13 @@ class _CrawlAsync { /// Add all nodes in the graph to [result] and return a Future which fires /// after all nodes have been seen. Future run() async { - await Future.wait(roots.map(_visit)); - await result.close(); + try { + await Future.wait(roots.map(_visit), eagerError: true); + await result.close(); + } catch (e, st) { + result.addError(e, st); + await result.close(); + } } /// Resolve the node at [key] and output it, then start crawling all of it's @@ -53,9 +62,10 @@ class _CrawlAsync { Future _crawlFrom(K key) async { var value = await readNode(key); if (value == null) return; + if (result.isClosed) return; result.add(value); var next = await children(key, value) ?? const []; - await Future.wait(next.map(_visit)); + await Future.wait(next.map(_visit), eagerError: true); } /// Synchronously record that [key] is being handled then start work on the diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index e57eebdc16..31db48d637 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,5 +1,5 @@ name: graphs -version: 0.1.1 +version: 0.1.2 description: Graph algorithms operation an graphs in any representation. author: Dart Team homepage: https://github.com/dart-lang/graphs diff --git a/pkgs/graphs/test/crawl_async_test.dart b/pkgs/graphs/test/crawl_async_test.dart index a4a3ff2324..8f482c4a8a 100644 --- a/pkgs/graphs/test/crawl_async_test.dart +++ b/pkgs/graphs/test/crawl_async_test.dart @@ -84,5 +84,23 @@ void main() { ]); expect(result, ['a']); }); + + test('surfaces exceptions for crawling children', () { + var graph = { + 'a': ['b'], + }; + var nodes = crawlAsync(['a'], (n) => n, + (k, n) => k == 'b' ? throw new ArgumentError() : graph[k]); + expect(nodes, emitsThrough(emitsError(isArgumentError))); + }); + + test('surfaces exceptions for resolving keys', () { + var graph = { + 'a': ['b'], + }; + var nodes = crawlAsync(['a'], + (n) => n == 'b' ? throw new ArgumentError() : n, (k, n) => graph[k]); + expect(nodes, emitsThrough(emitsError(isArgumentError))); + }); }); } From 660aba1f877466764d24aab9bfd5d829724ce363 Mon Sep 17 00:00:00 2001 From: Konstantin Shcheglov Date: Fri, 25 May 2018 15:24:11 -0700 Subject: [PATCH 17/88] Use new analyzer APIs. --- pkgs/graphs/analysis_options.yaml | 4 +- pkgs/graphs/example/crawl_async_example.dart | 77 +++++++++++++++----- pkgs/graphs/pubspec.yaml | 5 +- 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/pkgs/graphs/analysis_options.yaml b/pkgs/graphs/analysis_options.yaml index a6125b9bf1..faa3ef955b 100644 --- a/pkgs/graphs/analysis_options.yaml +++ b/pkgs/graphs/analysis_options.yaml @@ -47,6 +47,6 @@ linter: - slash_for_doc_comments - type_init_formals - unnecessary_brace_in_string_interps - - unnecessary_getter_setters - - unnecessary_lambda + - unnecessary_getters_setters + - unnecessary_lambdas - unnecessary_null_aware_assignments diff --git a/pkgs/graphs/example/crawl_async_example.dart b/pkgs/graphs/example/crawl_async_example.dart index b9d91e3c58..afe6dcc643 100644 --- a/pkgs/graphs/example/crawl_async_example.dart +++ b/pkgs/graphs/example/crawl_async_example.dart @@ -3,32 +3,71 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:isolate'; -import 'package:analyzer/analyzer.dart' show parseDirectives; -import 'package:analyzer/dart/ast/ast.dart' show UriBasedDirective; -import 'package:resource/resource.dart'; +import 'package:analyzer/dart/analysis/analysis_context.dart'; +import 'package:analyzer/dart/analysis/context_builder.dart'; +import 'package:analyzer/dart/analysis/context_locator.dart'; +import 'package:analyzer/dart/ast/ast.dart'; import 'package:graphs/graphs.dart'; import 'package:path/path.dart' as p; -class Source { - final Uri uri; - final String content; - - Source(this.uri, this.content); +/// Print a transitive set of imported URIs where libraries are read +/// asynchronously. +Future main() async { + var allImports = await crawlAsync( + [Uri.parse('package:graphs/graphs.dart')], read, findImports) + .toList(); + print(allImports.map((s) => s.uri).toList()); } -Future read(Uri uri) async => - new Source(uri, await new Resource(uri).readAsString()); +AnalysisContext _analysisContext; + +Future get analysisContext async { + if (_analysisContext == null) { + var libUri = Uri.parse('package:graphs/'); + var libPath = await getFilePath(libUri); + var packagePath = p.dirname(libPath); + + var roots = new ContextLocator().locateRoots(includedPaths: [packagePath]); + if (roots.length != 1) { + throw new StateError( + 'Expected to find exactly one context root, got $roots'); + } + + _analysisContext = + new ContextBuilder().createContext(contextRoot: roots[0]); + } + + return _analysisContext; +} -Iterable findImports(Uri from, Source source) { - final unit = parseDirectives(source.content); - return unit.directives +Future> findImports(Uri from, Source source) async { + return source.unit.directives .where((d) => d is UriBasedDirective) .map((d) => (d as UriBasedDirective).uri.stringValue) .where((uri) => !uri.startsWith('dart:')) .map((import) => resolveImport(import, from)); } +Future getFilePath(Uri uri) async { + var fileUri = await Isolate.resolvePackageUri(uri); + if (!fileUri.isScheme('file')) { + throw new StateError( + 'Expected to resolve $uri to a file URI, got $fileUri'); + } + return p.fromUri(fileUri); +} + +Future parseFile(Uri uri) async { + var path = await getFilePath(uri); + var analysisSession = (await analysisContext).currentSession; + var parseResult = analysisSession.getParsedAstSync(path); + return parseResult.unit; +} + +Future read(Uri uri) async => new Source(uri, await parseFile(uri)); + Uri resolveImport(String import, Uri from) { if (import.startsWith('package:')) return Uri.parse(import); assert(from.scheme == 'package'); @@ -38,11 +77,9 @@ Uri resolveImport(String import, Uri from) { return Uri.parse('package:${p.join(package, path)}'); } -/// Print a transitive set of imported URIs where libraries are read -/// asynchronously. -Future main() async { - var allImports = await crawlAsync( - [Uri.parse('package:graphs/graphs.dart')], read, findImports) - .toList(); - print(allImports.map((s) => s.uri).toList()); +class Source { + final Uri uri; + final CompilationUnit unit; + + Source(this.uri, this.unit); } diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 31db48d637..26994d6688 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -7,11 +7,8 @@ homepage: https://github.com/dart-lang/graphs environment: sdk: '>=1.24.0 <2.0.0' -dependencies: - dev_dependencies: test: ^0.12.0 # For examples - analyzer: ^0.30.0 + analyzer: ^0.32.0 path: ^1.1.0 - resource: ^2.1.0 From 6d3dac3edd6b9232b3c842a350a6dca5dbda8aaf Mon Sep 17 00:00:00 2001 From: Konstantin Shcheglov Date: Fri, 25 May 2018 20:31:48 -0700 Subject: [PATCH 18/88] Fix naming nits. --- pkgs/graphs/example/crawl_async_example.dart | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pkgs/graphs/example/crawl_async_example.dart b/pkgs/graphs/example/crawl_async_example.dart index afe6dcc643..d5cf012a49 100644 --- a/pkgs/graphs/example/crawl_async_example.dart +++ b/pkgs/graphs/example/crawl_async_example.dart @@ -26,7 +26,7 @@ AnalysisContext _analysisContext; Future get analysisContext async { if (_analysisContext == null) { var libUri = Uri.parse('package:graphs/'); - var libPath = await getFilePath(libUri); + var libPath = await pathForUri(libUri); var packagePath = p.dirname(libPath); var roots = new ContextLocator().locateRoots(includedPaths: [packagePath]); @@ -50,23 +50,23 @@ Future> findImports(Uri from, Source source) async { .map((import) => resolveImport(import, from)); } -Future getFilePath(Uri uri) async { +Future parseUri(Uri uri) async { + var path = await pathForUri(uri); + var analysisSession = (await analysisContext).currentSession; + var parseResult = analysisSession.getParsedAstSync(path); + return parseResult.unit; +} + +Future pathForUri(Uri uri) async { var fileUri = await Isolate.resolvePackageUri(uri); - if (!fileUri.isScheme('file')) { + if (fileUri == null || !fileUri.isScheme('file')) { throw new StateError( 'Expected to resolve $uri to a file URI, got $fileUri'); } return p.fromUri(fileUri); } -Future parseFile(Uri uri) async { - var path = await getFilePath(uri); - var analysisSession = (await analysisContext).currentSession; - var parseResult = analysisSession.getParsedAstSync(path); - return parseResult.unit; -} - -Future read(Uri uri) async => new Source(uri, await parseFile(uri)); +Future read(Uri uri) async => new Source(uri, await parseUri(uri)); Uri resolveImport(String import, Uri from) { if (import.startsWith('package:')) return Uri.parse(import); From f69e424b07b354c73ac227133e43d1d7d3e774d5 Mon Sep 17 00:00:00 2001 From: Konstantin Shcheglov Date: Fri, 25 May 2018 20:32:29 -0700 Subject: [PATCH 19/88] Try to require sdk >=2.0.0-dev.48.0 to see if this fixes Travis. --- pkgs/graphs/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 26994d6688..68cf02ad61 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -5,7 +5,7 @@ author: Dart Team homepage: https://github.com/dart-lang/graphs environment: - sdk: '>=1.24.0 <2.0.0' + sdk: '>=2.0.0-dev.48.0 <2.0.0' dev_dependencies: test: ^0.12.0 From 4132632a7332641364146e805d29e26d3d8380b9 Mon Sep 17 00:00:00 2001 From: Konstantin Shcheglov Date: Fri, 25 May 2018 20:36:25 -0700 Subject: [PATCH 20/88] We cannot require sdk >=2.0.0-dev.48.0, because of stable SDK branch? --- pkgs/graphs/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 68cf02ad61..26994d6688 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -5,7 +5,7 @@ author: Dart Team homepage: https://github.com/dart-lang/graphs environment: - sdk: '>=2.0.0-dev.48.0 <2.0.0' + sdk: '>=1.24.0 <2.0.0' dev_dependencies: test: ^0.12.0 From 48c48ccb05e4717293a572de9f1dc84fcc7d605a Mon Sep 17 00:00:00 2001 From: Konstantin Shcheglov Date: Fri, 25 May 2018 21:25:14 -0700 Subject: [PATCH 21/88] Stop testing on stable, and require sdk >=2.0.0-dev.48.0 --- pkgs/graphs/.travis.yml | 1 - pkgs/graphs/pubspec.yaml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pkgs/graphs/.travis.yml b/pkgs/graphs/.travis.yml index fbdbe35148..eb143392a6 100644 --- a/pkgs/graphs/.travis.yml +++ b/pkgs/graphs/.travis.yml @@ -1,6 +1,5 @@ language: dart dart: - - stable - dev dart_task: diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 26994d6688..68cf02ad61 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -5,7 +5,7 @@ author: Dart Team homepage: https://github.com/dart-lang/graphs environment: - sdk: '>=1.24.0 <2.0.0' + sdk: '>=2.0.0-dev.48.0 <2.0.0' dev_dependencies: test: ^0.12.0 From 00cf4b0e7f779fead157375adaa59917a145e460 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Wed, 18 Jul 2018 15:18:33 -0700 Subject: [PATCH 22/88] Allow the non-dev Dart 2 SDK (#21) Closes #20 --- pkgs/graphs/CHANGELOG.md | 4 ++++ pkgs/graphs/pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index 6fcff6a639..76a1048a38 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.1.2+1 + +- Allow using non-dev Dart 2 SDK. + # 0.1.2 - `crawlAsync` surfaces exceptions while crawling through the result stream diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 68cf02ad61..6421b0679b 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,11 +1,11 @@ name: graphs -version: 0.1.2 +version: 0.1.2+1 description: Graph algorithms operation an graphs in any representation. author: Dart Team homepage: https://github.com/dart-lang/graphs environment: - sdk: '>=2.0.0-dev.48.0 <2.0.0' + sdk: '>=2.0.0-dev.48.0 <3.0.0' dev_dependencies: test: ^0.12.0 From fc9f63609d3c169ddf4dc03f989631b7b67881ae Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Thu, 9 Aug 2018 12:06:20 -0700 Subject: [PATCH 23/88] Update for Dart 2 (#22) - `dartfmt --fix` to drop optional `new`. - Change example to use `whereType`. - Update minimum SDK to one that has Dart 2 semantics by default. - Test on stable. - Update package:test dependency. --- pkgs/graphs/.travis.yml | 1 + pkgs/graphs/example/crawl_async_example.dart | 17 +++++++---------- pkgs/graphs/example/example.dart | 10 +++++----- pkgs/graphs/lib/src/crawl_async.dart | 8 ++++---- .../lib/src/strongly_connected_components.dart | 4 ++-- pkgs/graphs/pubspec.yaml | 6 +++--- pkgs/graphs/test/crawl_async_test.dart | 8 ++++---- .../strongly_connected_components_test.dart | 4 ++-- 8 files changed, 28 insertions(+), 30 deletions(-) diff --git a/pkgs/graphs/.travis.yml b/pkgs/graphs/.travis.yml index eb143392a6..f21132497e 100644 --- a/pkgs/graphs/.travis.yml +++ b/pkgs/graphs/.travis.yml @@ -1,6 +1,7 @@ language: dart dart: - dev + - stable dart_task: - test diff --git a/pkgs/graphs/example/crawl_async_example.dart b/pkgs/graphs/example/crawl_async_example.dart index d5cf012a49..1d4f87774a 100644 --- a/pkgs/graphs/example/crawl_async_example.dart +++ b/pkgs/graphs/example/crawl_async_example.dart @@ -29,14 +29,12 @@ Future get analysisContext async { var libPath = await pathForUri(libUri); var packagePath = p.dirname(libPath); - var roots = new ContextLocator().locateRoots(includedPaths: [packagePath]); + var roots = ContextLocator().locateRoots(includedPaths: [packagePath]); if (roots.length != 1) { - throw new StateError( - 'Expected to find exactly one context root, got $roots'); + throw StateError('Expected to find exactly one context root, got $roots'); } - _analysisContext = - new ContextBuilder().createContext(contextRoot: roots[0]); + _analysisContext = ContextBuilder().createContext(contextRoot: roots[0]); } return _analysisContext; @@ -44,8 +42,8 @@ Future get analysisContext async { Future> findImports(Uri from, Source source) async { return source.unit.directives - .where((d) => d is UriBasedDirective) - .map((d) => (d as UriBasedDirective).uri.stringValue) + .whereType() + .map((d) => d.uri.stringValue) .where((uri) => !uri.startsWith('dart:')) .map((import) => resolveImport(import, from)); } @@ -60,13 +58,12 @@ Future parseUri(Uri uri) async { Future pathForUri(Uri uri) async { var fileUri = await Isolate.resolvePackageUri(uri); if (fileUri == null || !fileUri.isScheme('file')) { - throw new StateError( - 'Expected to resolve $uri to a file URI, got $fileUri'); + throw StateError('Expected to resolve $uri to a file URI, got $fileUri'); } return p.fromUri(fileUri); } -Future read(Uri uri) async => new Source(uri, await parseUri(uri)); +Future read(Uri uri) async => Source(uri, await parseUri(uri)); Uri resolveImport(String import, Uri from) { if (import.startsWith('package:')) return Uri.parse(import); diff --git a/pkgs/graphs/example/example.dart b/pkgs/graphs/example/example.dart index ce37128467..743b716785 100644 --- a/pkgs/graphs/example/example.dart +++ b/pkgs/graphs/example/example.dart @@ -29,11 +29,11 @@ class Node { } void main() { - var nodeA = new Node('A', 1); - var nodeB = new Node('B', 2); - var nodeC = new Node('C', 3); - var nodeD = new Node('D', 4); - var graph = new Graph({ + var nodeA = Node('A', 1); + var nodeB = Node('B', 2); + var nodeC = Node('C', 3); + var nodeD = Node('D', 4); + var graph = Graph({ nodeA: [nodeB, nodeC], nodeB: [nodeC, nodeD], nodeC: [nodeB, nodeD] diff --git a/pkgs/graphs/lib/src/crawl_async.dart b/pkgs/graphs/lib/src/crawl_async.dart index 243cc99fd5..f70ba4316f 100644 --- a/pkgs/graphs/lib/src/crawl_async.dart +++ b/pkgs/graphs/lib/src/crawl_async.dart @@ -4,7 +4,7 @@ import 'dart:async'; -final _empty = new Future.value(null); +final _empty = Future.value(null); /// Finds and returns every node in a graph who's nodes and edges are /// asynchronously resolved. @@ -30,18 +30,18 @@ final _empty = new Future.value(null); /// work may have already been started. Stream crawlAsync(Iterable roots, FutureOr Function(K) readNode, FutureOr> Function(K, V) children) { - final crawl = new _CrawlAsync(roots, readNode, children)..run(); + final crawl = _CrawlAsync(roots, readNode, children)..run(); return crawl.result.stream; } class _CrawlAsync { - final result = new StreamController(); + final result = StreamController(); final FutureOr Function(K) readNode; final FutureOr> Function(K, V) children; final Iterable roots; - final _seen = new Set(); + final _seen = Set(); _CrawlAsync(this.roots, this.readNode, this.children); diff --git a/pkgs/graphs/lib/src/strongly_connected_components.dart b/pkgs/graphs/lib/src/strongly_connected_components.dart index a68e25afb7..893aee0d3a 100644 --- a/pkgs/graphs/lib/src/strongly_connected_components.dart +++ b/pkgs/graphs/lib/src/strongly_connected_components.dart @@ -28,10 +28,10 @@ List> stronglyConnectedComponents( final result = >[]; final lowLinks = {}; final indexes = {}; - final onStack = new Set(); + final onStack = Set(); var index = 0; - var lastVisited = new Queue(); + var lastVisited = Queue(); void strongConnect(V node) { var nodeKey = key(node); diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 6421b0679b..318c31e84b 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,14 +1,14 @@ name: graphs -version: 0.1.2+1 +version: 0.1.3-dev description: Graph algorithms operation an graphs in any representation. author: Dart Team homepage: https://github.com/dart-lang/graphs environment: - sdk: '>=2.0.0-dev.48.0 <3.0.0' + sdk: '>=2.0.0-dev.64.0 <3.0.0' dev_dependencies: - test: ^0.12.0 + test: ^1.0.0 # For examples analyzer: ^0.32.0 path: ^1.1.0 diff --git a/pkgs/graphs/test/crawl_async_test.dart b/pkgs/graphs/test/crawl_async_test.dart index 8f482c4a8a..64b4dbd0f1 100644 --- a/pkgs/graphs/test/crawl_async_test.dart +++ b/pkgs/graphs/test/crawl_async_test.dart @@ -14,7 +14,7 @@ void main() { group('asyncCrawl', () { Future> crawl( Map> g, Iterable roots) { - var graph = new AsyncGraph(g); + var graph = AsyncGraph(g); return crawlAsync(roots, graph.readNode, graph.children).toList(); } @@ -90,7 +90,7 @@ void main() { 'a': ['b'], }; var nodes = crawlAsync(['a'], (n) => n, - (k, n) => k == 'b' ? throw new ArgumentError() : graph[k]); + (k, n) => k == 'b' ? throw ArgumentError() : graph[k]); expect(nodes, emitsThrough(emitsError(isArgumentError))); }); @@ -98,8 +98,8 @@ void main() { var graph = { 'a': ['b'], }; - var nodes = crawlAsync(['a'], - (n) => n == 'b' ? throw new ArgumentError() : n, (k, n) => graph[k]); + var nodes = crawlAsync(['a'], (n) => n == 'b' ? throw ArgumentError() : n, + (k, n) => graph[k]); expect(nodes, emitsThrough(emitsError(isArgumentError))); }); }); diff --git a/pkgs/graphs/test/strongly_connected_components_test.dart b/pkgs/graphs/test/strongly_connected_components_test.dart index 34d8355fbc..0e2d97770e 100644 --- a/pkgs/graphs/test/strongly_connected_components_test.dart +++ b/pkgs/graphs/test/strongly_connected_components_test.dart @@ -11,7 +11,7 @@ void main() { group('strongly connected components', () { /// Run [stronglyConnectedComponents] on [g]. List> components(Map> g) { - final graph = new Graph(g); + final graph = Graph(g); return stronglyConnectedComponents( graph.allNodes, graph.key, graph.children); } @@ -53,7 +53,7 @@ void main() { test('includes the first passed root last in a cycle', () { // In cases where this is used to find a topological ordering the first // value in nodes should always come last. - var graph = new Graph({ + var graph = Graph({ 'a': ['b'], 'b': ['a'] }); From 690e451bee6a15ddc84591f7a83cc486462ed58b Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Tue, 4 Sep 2018 21:38:01 -0700 Subject: [PATCH 24/88] Add more lints used in package:pedantic (#24) Closes #23 --- pkgs/graphs/analysis_options.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkgs/graphs/analysis_options.yaml b/pkgs/graphs/analysis_options.yaml index faa3ef955b..87d9ab92e0 100644 --- a/pkgs/graphs/analysis_options.yaml +++ b/pkgs/graphs/analysis_options.yaml @@ -10,6 +10,7 @@ linter: rules: # Errors - avoid_empty_else + - avoid_types_as_parameter_names - await_only_futures - comment_references - control_flow_in_finally @@ -28,6 +29,7 @@ linter: - annotate_overrides - avoid_function_literals_in_foreach_calls - avoid_init_to_null + - avoid_relative_lib_imports - avoid_return_types_on_setters - avoid_returning_null - avoid_unused_constructor_parameters @@ -39,6 +41,8 @@ linter: - library_prefixes - non_constant_identifier_names - prefer_conditional_assignment + - prefer_contains + - prefer_equal_for_default_values - prefer_final_fields - prefer_is_empty - prefer_is_not_empty @@ -50,3 +54,4 @@ linter: - unnecessary_getters_setters - unnecessary_lambdas - unnecessary_null_aware_assignments + - use_rethrow_when_possible From 72974f2a880153d2a78d345b1ef4fd805d435f18 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Mon, 5 Nov 2018 13:53:54 -0800 Subject: [PATCH 25/88] Use HashMap/HashSet for stronglyConnectedComponents (#26) Looks like ~10% improvement with dart2js and a ~8% improvement on VM Fixes https://github.com/dart-lang/graphs/issues/25 --- pkgs/graphs/CHANGELOG.md | 5 +++++ pkgs/graphs/lib/src/strongly_connected_components.dart | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index 76a1048a38..ad0f971c29 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,3 +1,8 @@ +# 0.1.3 + +- Use `HashMap` and `HashSet` from `dart:collection` for + `stronglyConnectedComponents`. Improves runtime performance. + # 0.1.2+1 - Allow using non-dev Dart 2 SDK. diff --git a/pkgs/graphs/lib/src/strongly_connected_components.dart b/pkgs/graphs/lib/src/strongly_connected_components.dart index 893aee0d3a..4866c8ce2f 100644 --- a/pkgs/graphs/lib/src/strongly_connected_components.dart +++ b/pkgs/graphs/lib/src/strongly_connected_components.dart @@ -26,9 +26,9 @@ import 'dart:math' show min; List> stronglyConnectedComponents( Iterable nodes, K Function(V) key, Iterable Function(V) children) { final result = >[]; - final lowLinks = {}; - final indexes = {}; - final onStack = Set(); + final lowLinks = HashMap(); + final indexes = HashMap(); + final onStack = HashSet(); var index = 0; var lastVisited = Queue(); From 24989172b8adae1f049d45f767f4417379653a84 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 6 Nov 2018 08:54:08 -0800 Subject: [PATCH 26/88] Add shortestPath(s) functions Fixes https://github.com/dart-lang/graphs/issues/27 --- pkgs/graphs/CHANGELOG.md | 1 + .../benchmark/shortest_path_benchmark.dart | 45 ++++++++ pkgs/graphs/lib/graphs.dart | 1 + pkgs/graphs/lib/src/shortest_path.dart | 98 ++++++++++++++++ pkgs/graphs/test/shortest_path_test.dart | 109 ++++++++++++++++++ 5 files changed, 254 insertions(+) create mode 100644 pkgs/graphs/benchmark/shortest_path_benchmark.dart create mode 100644 pkgs/graphs/lib/src/shortest_path.dart create mode 100644 pkgs/graphs/test/shortest_path_test.dart diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index ad0f971c29..0759d0fe63 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,5 +1,6 @@ # 0.1.3 +- Added `shortestPath` and `shortestPaths` functions. - Use `HashMap` and `HashSet` from `dart:collection` for `stronglyConnectedComponents`. Improves runtime performance. diff --git a/pkgs/graphs/benchmark/shortest_path_benchmark.dart b/pkgs/graphs/benchmark/shortest_path_benchmark.dart new file mode 100644 index 0000000000..106e5a9299 --- /dev/null +++ b/pkgs/graphs/benchmark/shortest_path_benchmark.dart @@ -0,0 +1,45 @@ +// 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. + +import 'dart:collection'; +import 'dart:math' show Random; + +import 'package:graphs/graphs.dart'; + +void main() { + final _rnd = Random(1); + final size = 1000; + final graph = HashMap>(); + + for (var i = 0; i < size * 5; i++) { + final toList = graph.putIfAbsent(_rnd.nextInt(size), () => List()); + + final toValue = _rnd.nextInt(size); + if (!toList.contains(toValue)) { + toList.add(toValue); + } + } + + var counts = []; + + final testOutput = + shortestPath(0, size - 1, (v) => v, (e) => graph[e] ?? []).toString(); + print(testOutput); + assert(testOutput == '[258, 252, 819, 999]'); + + for (var i = 0; i < 50; i++) { + var count = 0; + final watch = Stopwatch()..start(); + while (watch.elapsed < const Duration(milliseconds: 100)) { + count++; + final length = + shortestPath(0, size - 1, (v) => v, (e) => graph[e] ?? []).length; + assert(length == 4, '$length'); + } + print(count); + counts.add(count); + } + + print('max iterations in 1s: ${(counts..sort()).last}'); +} diff --git a/pkgs/graphs/lib/graphs.dart b/pkgs/graphs/lib/graphs.dart index 8f55504bab..a5de89cfa7 100644 --- a/pkgs/graphs/lib/graphs.dart +++ b/pkgs/graphs/lib/graphs.dart @@ -3,5 +3,6 @@ // BSD-style license that can be found in the LICENSE file. export 'src/crawl_async.dart' show crawlAsync; +export 'src/shortest_path.dart' show shortestPath, shortestPaths; export 'src/strongly_connected_components.dart' show stronglyConnectedComponents; diff --git a/pkgs/graphs/lib/src/shortest_path.dart b/pkgs/graphs/lib/src/shortest_path.dart new file mode 100644 index 0000000000..657fe5de88 --- /dev/null +++ b/pkgs/graphs/lib/src/shortest_path.dart @@ -0,0 +1,98 @@ +// 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. + +import 'dart:collection'; + +/// Returns the shortest path from [start] to [target] given the directed +/// edges of a graph provided by [edges]. +/// +/// If [start] `==` [target], an empty [List] is returned and [edges] is never +/// called. +/// +/// [V] is the type of values in the graph nodes. [K] must be a type suitable +/// for using as a Map or Set key, and [key] must provide a consistent key for +/// every node. +/// +/// [start], [target] and all values returned by [edges] must not be `null`. +/// If asserts are enabled, an [AssertionError] is raised if these conditions +/// are not met. If asserts are not enabled, violations result in undefined +/// behavior. +List shortestPath( + V start, V target, K Function(V) key, Iterable Function(V) edges) => + _shortestPaths(start, key, edges, target)[key(target)]; + +List shortestPathWithKey( + V start, V target, K Function(V) key, Iterable Function(V) edges) => + _shortestPaths(start, key, edges, target)[key(target)]; + +/// Returns a [Map] of the shortest paths from [start] to all of the nodes in +/// the directed graph defined by [edges]. +/// +/// All return values will contain the key [start] with an empty [List] value. +/// +/// [V] is the type of values in the graph nodes. [K] must be a type suitable +/// for using as a Map or Set key, and [key] must provide a consistent key for +/// every node. +/// +/// [start] and all values returned by [edges] must not be `null`. +/// If asserts are enabled, an [AssertionError] is raised if these conditions +/// are not met. If asserts are not enabled, violations result in undefined +/// behavior. +Map> shortestPaths( + V start, K Function(V) key, Iterable Function(V) edges) => + _shortestPaths(start, key, edges); + +Map> _shortestPaths( + V start, K Function(V) key, Iterable Function(V) edges, + [V target]) { + assert(start != null, '`start` cannot be null'); + assert(key != null, '`key` cannot be null`.'); + assert(edges != null, '`edges` cannot be null'); + + final distances = HashMap>(); + distances[key(start)] = []; + + if (start == target) { + return distances; + } + + final toVisit = ListQueue()..add(start); + + List bestOption; + + while (toVisit.isNotEmpty) { + final current = toVisit.removeFirst(); + final distanceToCurrent = distances[key(current)]; + + if (bestOption != null && distanceToCurrent.length >= bestOption.length) { + // Skip any existing `toVisit` items that have no chance of being + // better than bestOption (if it exists) + continue; + } + + for (var edge in edges(current)) { + assert(edge != null, '`edges` cannot return null values.'); + final existingPath = distances[key(edge)]; + + if (existingPath == null || + existingPath.length > (distanceToCurrent.length + 1)) { + final newOption = distanceToCurrent.followedBy([edge]).toList(); + + if (edge == target) { + assert(bestOption == null || bestOption.length > newOption.length); + bestOption = newOption; + } + + distances[key(edge)] = newOption; + if (bestOption == null || bestOption.length > newOption.length) { + // Only add a node to visit if it might be a better path to the + // target node + toVisit.add(edge); + } + } + } + } + + return distances; +} diff --git a/pkgs/graphs/test/shortest_path_test.dart b/pkgs/graphs/test/shortest_path_test.dart new file mode 100644 index 0000000000..74d7acbb21 --- /dev/null +++ b/pkgs/graphs/test/shortest_path_test.dart @@ -0,0 +1,109 @@ +// 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. + +import 'package:graphs/graphs.dart'; +import 'package:test/test.dart'; + +Matcher _throwsAssertionError(messageMatcher) => + throwsA(const TypeMatcher() + .having((ae) => ae.message, 'message', messageMatcher)); + +String _identity(int input) => input.toString(); + +void main() { + const graph = >{ + 1: [2, 5], + 2: [3], + 3: [4, 5], + 4: [1], + 5: [8], + 6: [7], + }; + + test('null `start` throws AssertionError', () { + expect( + () => shortestPath( + null, 1, _identity, (input) => graph[input] ?? []), + _throwsAssertionError('`start` cannot be null')); + expect( + () => shortestPaths( + null, _identity, (input) => graph[input] ?? []), + _throwsAssertionError('`start` cannot be null')); + }); + + test('null `edges` throws AssertionError', () { + expect(() => shortestPath(1, 1, _identity, null), + _throwsAssertionError('`edges` cannot be null')); + expect(() => shortestPaths(1, _identity, null), + _throwsAssertionError('`edges` cannot be null')); + }); + + test('null return value from `edges` throws', () { + expect(shortestPath(1, 1, _identity, (input) => null), [], + reason: 'self target short-circuits'); + expect(shortestPath(1, 1, _identity, (input) => [null]), [], + reason: 'self target short-circuits'); + + expect(() => shortestPath(1, 2, _identity, (input) => null), + throwsNoSuchMethodError); + + expect(() => shortestPaths(1, _identity, (input) => null), + throwsNoSuchMethodError); + + expect(() => shortestPath(1, 2, _identity, (input) => [null]), + _throwsAssertionError('`edges` cannot return null values.')); + expect(() => shortestPaths(1, _identity, (input) => [null]), + _throwsAssertionError('`edges` cannot return null values.')); + }); + + void _singlePathTest(int from, int to, List expected) { + test('$from -> $to should be $expected', () { + expect( + shortestPath( + from, to, _identity, (input) => graph[input] ?? []), + expected); + }); + } + + void _pathsTest( + int from, Map> expected, List nullPaths) { + test('paths from $from', () { + final result = shortestPaths( + from, _identity, (input) => graph[input] ?? []); + expect(result, expected); + }); + + for (var entry in expected.entries) { + _singlePathTest(from, int.parse(entry.key), entry.value); + } + + for (var entry in nullPaths) { + _singlePathTest(from, entry, null); + } + } + + _pathsTest(1, { + '5': [5], + '3': [2, 3], + '8': [5, 8], + '1': [], + '2': [2], + '4': [2, 3, 4], + }, [ + 6, + 7, + ]); + + _pathsTest(6, { + '7': [7], + '6': [], + }, [ + 1, + ]); + _pathsTest(7, {'7': []}, [1, 6]); + + _pathsTest(42, {'42': []}, [1, 6]); + + _singlePathTest(1, null, null); +} From 91e320b2f162e76f1fb82581112a99bb2df230cb Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 6 Nov 2018 10:28:37 -0800 Subject: [PATCH 27/88] update dev dependencies --- pkgs/graphs/pubspec.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 318c31e84b..c603091bd0 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -8,7 +8,8 @@ environment: sdk: '>=2.0.0-dev.64.0 <3.0.0' dev_dependencies: - test: ^1.0.0 + test: ^1.5.1 + # For examples - analyzer: ^0.32.0 + analyzer: ^0.33.0 path: ^1.1.0 From 3eb499c3c8b6274b78cffa4c18bf3425fb91f772 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 6 Nov 2018 10:59:10 -0800 Subject: [PATCH 28/88] enable and fix a number of lints, use pedantic lints explicitly --- pkgs/graphs/.travis.yml | 6 +- pkgs/graphs/analysis_options.yaml | 61 +++++++++++-------- .../benchmark/shortest_path_benchmark.dart | 2 +- .../src/strongly_connected_components.dart | 1 + pkgs/graphs/pubspec.yaml | 1 + 5 files changed, 41 insertions(+), 30 deletions(-) diff --git a/pkgs/graphs/.travis.yml b/pkgs/graphs/.travis.yml index f21132497e..3c0743f6ed 100644 --- a/pkgs/graphs/.travis.yml +++ b/pkgs/graphs/.travis.yml @@ -1,14 +1,14 @@ language: dart + dart: - dev - stable dart_task: - test - - test -p chrome - - test -p firefox + - test -p chrome,firefox - dartfmt - - dartanalyzer + - dartanalyzer: --fatal-infos --fatal-warnings . matrix: exclude: diff --git a/pkgs/graphs/analysis_options.yaml b/pkgs/graphs/analysis_options.yaml index 87d9ab92e0..6e159f34f8 100644 --- a/pkgs/graphs/analysis_options.yaml +++ b/pkgs/graphs/analysis_options.yaml @@ -1,57 +1,66 @@ +include: package:pedantic/analysis_options.yaml analyzer: strong-mode: implicit-casts: false errors: + unused_element: error unused_import: error unused_local_variable: error dead_code: error - override_on_non_overriding_method: error linter: rules: - # Errors - - avoid_empty_else - - avoid_types_as_parameter_names - - await_only_futures - - comment_references - - control_flow_in_finally - - empty_statements - - hash_and_equals - - iterable_contains_unrelated_type - - no_duplicate_case_values - - test_types_in_equals - - throw_in_finally - - unawaited_futures - - unnecessary_statements - - unrelated_type_equality_checks - - valid_regexps - - # Style - annotate_overrides - avoid_function_literals_in_foreach_calls - avoid_init_to_null + - avoid_null_checks_in_equality_operators - avoid_relative_lib_imports - - avoid_return_types_on_setters - avoid_returning_null - avoid_unused_constructor_parameters + - await_only_futures - camel_case_types + - cancel_subscriptions + - comment_references + - constant_identifier_names + - control_flow_in_finally - directives_ordering - empty_catches - empty_constructor_bodies + - empty_statements + - hash_and_equals + - implementation_imports + - invariant_booleans + - iterable_contains_unrelated_type - library_names - library_prefixes + - list_remove_unrelated_type + - no_adjacent_strings_in_list - non_constant_identifier_names + - omit_local_variable_types + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - prefer_adjacent_string_concatenation + - prefer_collection_literals - prefer_conditional_assignment - - prefer_contains - - prefer_equal_for_default_values + - prefer_const_constructors - prefer_final_fields - - prefer_is_empty - - prefer_is_not_empty + - prefer_initializing_formals + - prefer_interpolation_to_compose_strings + - prefer_single_quotes - prefer_typing_uninitialized_variables - - recursive_getters - slash_for_doc_comments + - test_types_in_equals + - super_goes_last + - test_types_in_equals + - throw_in_finally - type_init_formals - unnecessary_brace_in_string_interps + - unnecessary_const - unnecessary_getters_setters - unnecessary_lambdas + - unnecessary_new - unnecessary_null_aware_assignments - - use_rethrow_when_possible + - unnecessary_statements + - unnecessary_this diff --git a/pkgs/graphs/benchmark/shortest_path_benchmark.dart b/pkgs/graphs/benchmark/shortest_path_benchmark.dart index 106e5a9299..f194d78e87 100644 --- a/pkgs/graphs/benchmark/shortest_path_benchmark.dart +++ b/pkgs/graphs/benchmark/shortest_path_benchmark.dart @@ -13,7 +13,7 @@ void main() { final graph = HashMap>(); for (var i = 0; i < size * 5; i++) { - final toList = graph.putIfAbsent(_rnd.nextInt(size), () => List()); + final toList = graph.putIfAbsent(_rnd.nextInt(size), () => []); final toValue = _rnd.nextInt(size); if (!toList.contains(toValue)) { diff --git a/pkgs/graphs/lib/src/strongly_connected_components.dart b/pkgs/graphs/lib/src/strongly_connected_components.dart index 4866c8ce2f..62cb6147ef 100644 --- a/pkgs/graphs/lib/src/strongly_connected_components.dart +++ b/pkgs/graphs/lib/src/strongly_connected_components.dart @@ -40,6 +40,7 @@ List> stronglyConnectedComponents( index++; lastVisited.addLast(node); onStack.add(nodeKey); + // ignore: omit_local_variable_types for (final V next in children(node) ?? const []) { var nextKey = key(next); if (!indexes.containsKey(nextKey)) { diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index c603091bd0..a2e0f5c900 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -8,6 +8,7 @@ environment: sdk: '>=2.0.0-dev.64.0 <3.0.0' dev_dependencies: + pedantic: ^1.3.0 test: ^1.5.1 # For examples From f2c6a241e54ab4bf2c7e3013d020bc2237cbcf8a Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 6 Nov 2018 10:27:55 -0800 Subject: [PATCH 29/88] prepare for release --- pkgs/graphs/pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index a2e0f5c900..b6b59d9368 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,11 +1,11 @@ name: graphs -version: 0.1.3-dev +version: 0.1.3 description: Graph algorithms operation an graphs in any representation. author: Dart Team homepage: https://github.com/dart-lang/graphs environment: - sdk: '>=2.0.0-dev.64.0 <3.0.0' + sdk: '>=2.0.0 <3.0.0' dev_dependencies: pedantic: ^1.3.0 From 2a427b2958a6e09336e0b82dde7e12c0b12cfee8 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Wed, 7 Nov 2018 22:01:41 -0800 Subject: [PATCH 30/88] Remove accidental API in src/shortest_path Never exposed publicly --- pkgs/graphs/lib/src/shortest_path.dart | 4 ---- pkgs/graphs/pubspec.yaml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/pkgs/graphs/lib/src/shortest_path.dart b/pkgs/graphs/lib/src/shortest_path.dart index 657fe5de88..a1083eb477 100644 --- a/pkgs/graphs/lib/src/shortest_path.dart +++ b/pkgs/graphs/lib/src/shortest_path.dart @@ -22,10 +22,6 @@ List shortestPath( V start, V target, K Function(V) key, Iterable Function(V) edges) => _shortestPaths(start, key, edges, target)[key(target)]; -List shortestPathWithKey( - V start, V target, K Function(V) key, Iterable Function(V) edges) => - _shortestPaths(start, key, edges, target)[key(target)]; - /// Returns a [Map] of the shortest paths from [start] to all of the nodes in /// the directed graph defined by [edges]. /// diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index b6b59d9368..cbafdaaafd 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,5 +1,5 @@ name: graphs -version: 0.1.3 +version: 0.1.4-dev description: Graph algorithms operation an graphs in any representation. author: Dart Team homepage: https://github.com/dart-lang/graphs From 9713e1dc65c2c72b7160822fc2275fd44a8f63e7 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Thu, 8 Nov 2018 06:58:06 -0800 Subject: [PATCH 31/88] Fix a bug with non-identity key in shortestPath functions made the tests more stronger --- pkgs/graphs/CHANGELOG.md | 4 ++ pkgs/graphs/lib/src/shortest_path.dart | 4 +- pkgs/graphs/test/shortest_path_test.dart | 63 ++++++++++++++---------- pkgs/graphs/test/utils/utils.dart | 18 +++++++ 4 files changed, 62 insertions(+), 27 deletions(-) create mode 100644 pkgs/graphs/test/utils/utils.dart diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index 0759d0fe63..3057d433be 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.1.4-dev + +- Fixed a bug with non-identity `key` in `shortestPath` and `shortestPaths`. + # 0.1.3 - Added `shortestPath` and `shortestPaths` functions. diff --git a/pkgs/graphs/lib/src/shortest_path.dart b/pkgs/graphs/lib/src/shortest_path.dart index a1083eb477..80c69b177b 100644 --- a/pkgs/graphs/lib/src/shortest_path.dart +++ b/pkgs/graphs/lib/src/shortest_path.dart @@ -49,7 +49,7 @@ Map> _shortestPaths( final distances = HashMap>(); distances[key(start)] = []; - if (start == target) { + if (target != null && key(start) == key(target)) { return distances; } @@ -75,7 +75,7 @@ Map> _shortestPaths( existingPath.length > (distanceToCurrent.length + 1)) { final newOption = distanceToCurrent.followedBy([edge]).toList(); - if (edge == target) { + if (target != null && key(edge) == key(target)) { assert(bestOption == null || bestOption.length > newOption.length); bestOption = newOption; } diff --git a/pkgs/graphs/test/shortest_path_test.dart b/pkgs/graphs/test/shortest_path_test.dart index 74d7acbb21..253296b5e2 100644 --- a/pkgs/graphs/test/shortest_path_test.dart +++ b/pkgs/graphs/test/shortest_path_test.dart @@ -5,11 +5,15 @@ import 'package:graphs/graphs.dart'; import 'package:test/test.dart'; +import 'utils/utils.dart'; + Matcher _throwsAssertionError(messageMatcher) => throwsA(const TypeMatcher() .having((ae) => ae.message, 'message', messageMatcher)); -String _identity(int input) => input.toString(); +int _xKey(X input) => input.value; + +T _identity(T input) => input; void main() { const graph = >{ @@ -21,14 +25,15 @@ void main() { 6: [7], }; + List getValues(int key) => graph[key] ?? []; + + List getXValues(X key) => + graph[key.value]?.map((v) => X(v))?.toList() ?? []; + test('null `start` throws AssertionError', () { - expect( - () => shortestPath( - null, 1, _identity, (input) => graph[input] ?? []), + expect(() => shortestPath(null, 1, _identity, getValues), _throwsAssertionError('`start` cannot be null')); - expect( - () => shortestPaths( - null, _identity, (input) => graph[input] ?? []), + expect(() => shortestPaths(null, _identity, getValues), _throwsAssertionError('`start` cannot be null')); }); @@ -58,24 +63,32 @@ void main() { }); void _singlePathTest(int from, int to, List expected) { - test('$from -> $to should be $expected', () { + test('$from -> $to should be $expected (mapped)', () { expect( - shortestPath( - from, to, _identity, (input) => graph[input] ?? []), + shortestPath(X(from), X(to), _xKey, getXValues) + ?.map((x) => x.value), expected); }); + + test('$from -> $to should be $expected', () { + expect(shortestPath(from, to, _identity, getValues), expected); + }); } - void _pathsTest( - int from, Map> expected, List nullPaths) { + void _pathsTest(int from, Map> expected, List nullPaths) { + test('paths from $from (mapped)', () { + final result = shortestPaths(X(from), _xKey, getXValues) + .map((k, v) => MapEntry(k, v.map((x) => x.value).toList())); + expect(result, expected); + }); + test('paths from $from', () { - final result = shortestPaths( - from, _identity, (input) => graph[input] ?? []); + final result = shortestPaths(from, _identity, getValues); expect(result, expected); }); for (var entry in expected.entries) { - _singlePathTest(from, int.parse(entry.key), entry.value); + _singlePathTest(from, entry.key, entry.value); } for (var entry in nullPaths) { @@ -84,26 +97,26 @@ void main() { } _pathsTest(1, { - '5': [5], - '3': [2, 3], - '8': [5, 8], - '1': [], - '2': [2], - '4': [2, 3, 4], + 5: [5], + 3: [2, 3], + 8: [5, 8], + 1: [], + 2: [2], + 4: [2, 3, 4], }, [ 6, 7, ]); _pathsTest(6, { - '7': [7], - '6': [], + 7: [7], + 6: [], }, [ 1, ]); - _pathsTest(7, {'7': []}, [1, 6]); + _pathsTest(7, {7: []}, [1, 6]); - _pathsTest(42, {'42': []}, [1, 6]); + _pathsTest(42, {42: []}, [1, 6]); _singlePathTest(1, null, null); } diff --git a/pkgs/graphs/test/utils/utils.dart b/pkgs/graphs/test/utils/utils.dart new file mode 100644 index 0000000000..828b91ebf7 --- /dev/null +++ b/pkgs/graphs/test/utils/utils.dart @@ -0,0 +1,18 @@ +// 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. + +class X { + final int value; + + X(this.value); + + @override + bool operator ==(Object other) => throw UnimplementedError(); + + @override + int get hashCode => 42; + + @override + String toString() => '($value)'; +} From 0c928c0112d98e3825a961b2305c4b9dfb39a34a Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Thu, 8 Nov 2018 07:15:23 -0800 Subject: [PATCH 32/88] shortestPath: calculate key(target) less --- pkgs/graphs/lib/src/shortest_path.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkgs/graphs/lib/src/shortest_path.dart b/pkgs/graphs/lib/src/shortest_path.dart index 80c69b177b..f8f06d0b4f 100644 --- a/pkgs/graphs/lib/src/shortest_path.dart +++ b/pkgs/graphs/lib/src/shortest_path.dart @@ -20,7 +20,7 @@ import 'dart:collection'; /// behavior. List shortestPath( V start, V target, K Function(V) key, Iterable Function(V) edges) => - _shortestPaths(start, key, edges, target)[key(target)]; + _shortestPaths(start, key, edges, key(target))[key(target)]; /// Returns a [Map] of the shortest paths from [start] to all of the nodes in /// the directed graph defined by [edges]. @@ -37,11 +37,11 @@ List shortestPath( /// behavior. Map> shortestPaths( V start, K Function(V) key, Iterable Function(V) edges) => - _shortestPaths(start, key, edges); + _shortestPaths(start, key, edges); Map> _shortestPaths( V start, K Function(V) key, Iterable Function(V) edges, - [V target]) { + [K targetKey]) { assert(start != null, '`start` cannot be null'); assert(key != null, '`key` cannot be null`.'); assert(edges != null, '`edges` cannot be null'); @@ -49,7 +49,7 @@ Map> _shortestPaths( final distances = HashMap>(); distances[key(start)] = []; - if (target != null && key(start) == key(target)) { + if (key(start) == targetKey) { return distances; } @@ -75,7 +75,7 @@ Map> _shortestPaths( existingPath.length > (distanceToCurrent.length + 1)) { final newOption = distanceToCurrent.followedBy([edge]).toList(); - if (target != null && key(edge) == key(target)) { + if (key(edge) == targetKey) { assert(bestOption == null || bestOption.length > newOption.length); bestOption = newOption; } From 93f85388afd7c3823a135a49b009028c083fe1c5 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Thu, 8 Nov 2018 07:15:46 -0800 Subject: [PATCH 33/88] shortest path benchmark: keep running until terminated --- pkgs/graphs/benchmark/shortest_path_benchmark.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pkgs/graphs/benchmark/shortest_path_benchmark.dart b/pkgs/graphs/benchmark/shortest_path_benchmark.dart index f194d78e87..8015cb19af 100644 --- a/pkgs/graphs/benchmark/shortest_path_benchmark.dart +++ b/pkgs/graphs/benchmark/shortest_path_benchmark.dart @@ -21,14 +21,14 @@ void main() { } } - var counts = []; + var mostPerSecond = 0; final testOutput = shortestPath(0, size - 1, (v) => v, (e) => graph[e] ?? []).toString(); print(testOutput); assert(testOutput == '[258, 252, 819, 999]'); - for (var i = 0; i < 50; i++) { + for (var i = 0;; i++) { var count = 0; final watch = Stopwatch()..start(); while (watch.elapsed < const Duration(milliseconds: 100)) { @@ -37,9 +37,11 @@ void main() { shortestPath(0, size - 1, (v) => v, (e) => graph[e] ?? []).length; assert(length == 4, '$length'); } - print(count); - counts.add(count); - } - print('max iterations in 1s: ${(counts..sort()).last}'); + if (count > mostPerSecond) { + mostPerSecond = count; + } + + print('max iterations in 1s: $mostPerSecond\tafter $i iterations'); + } } From cce128f04bb22d2d411957499e2b945dcbb25d19 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Thu, 8 Nov 2018 07:23:10 -0800 Subject: [PATCH 34/88] Use HashSet in crawlAsync --- pkgs/graphs/lib/src/crawl_async.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkgs/graphs/lib/src/crawl_async.dart b/pkgs/graphs/lib/src/crawl_async.dart index f70ba4316f..d15d56a225 100644 --- a/pkgs/graphs/lib/src/crawl_async.dart +++ b/pkgs/graphs/lib/src/crawl_async.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:collection'; final _empty = Future.value(null); @@ -41,7 +42,7 @@ class _CrawlAsync { final FutureOr> Function(K, V) children; final Iterable roots; - final _seen = Set(); + final _seen = HashSet(); _CrawlAsync(this.roots, this.readNode, this.children); From e062f90b07d679fa2da4b287f90b1cbdcbb046f7 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Thu, 8 Nov 2018 10:44:26 -0800 Subject: [PATCH 35/88] prepare to release v0.1.3+1 (#31) --- pkgs/graphs/CHANGELOG.md | 2 +- pkgs/graphs/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index 3057d433be..79b44a9ae0 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,4 +1,4 @@ -# 0.1.4-dev +# 0.1.3+1 - Fixed a bug with non-identity `key` in `shortestPath` and `shortestPaths`. diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index cbafdaaafd..0caee34057 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,5 +1,5 @@ name: graphs -version: 0.1.4-dev +version: 0.1.3+1 description: Graph algorithms operation an graphs in any representation. author: Dart Team homepage: https://github.com/dart-lang/graphs From a6327f97988bef87885ff7f4b8f785509e48f45d Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Thu, 8 Nov 2018 08:29:23 -0800 Subject: [PATCH 36/88] Small tweaks to shortest path benchmark output --- .../benchmark/shortest_path_benchmark.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pkgs/graphs/benchmark/shortest_path_benchmark.dart b/pkgs/graphs/benchmark/shortest_path_benchmark.dart index 8015cb19af..1b2e0de5e6 100644 --- a/pkgs/graphs/benchmark/shortest_path_benchmark.dart +++ b/pkgs/graphs/benchmark/shortest_path_benchmark.dart @@ -21,27 +21,34 @@ void main() { } } - var mostPerSecond = 0; + var maxCount = 0; + var maxIteration = 0; final testOutput = shortestPath(0, size - 1, (v) => v, (e) => graph[e] ?? []).toString(); print(testOutput); assert(testOutput == '[258, 252, 819, 999]'); - for (var i = 0;; i++) { + final duration = const Duration(milliseconds: 100); + + for (var i = 1;; i++) { var count = 0; final watch = Stopwatch()..start(); - while (watch.elapsed < const Duration(milliseconds: 100)) { + while (watch.elapsed < duration) { count++; final length = shortestPath(0, size - 1, (v) => v, (e) => graph[e] ?? []).length; assert(length == 4, '$length'); } - if (count > mostPerSecond) { - mostPerSecond = count; + if (count > maxCount) { + maxCount = count; + maxIteration = i; } - print('max iterations in 1s: $mostPerSecond\tafter $i iterations'); + if (maxIteration == i || (i - maxIteration) % 20 == 0) { + print('max iterations in ${duration.inMilliseconds}ms: $maxCount\t' + 'after $maxIteration of $i iterations'); + } } } From 2589919718fd66d029fb86c917900a9c04cdc417 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Thu, 8 Nov 2018 09:18:11 -0800 Subject: [PATCH 37/88] Add connected component benchmark --- .../connected_components_benchmark.dart | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 pkgs/graphs/benchmark/connected_components_benchmark.dart diff --git a/pkgs/graphs/benchmark/connected_components_benchmark.dart b/pkgs/graphs/benchmark/connected_components_benchmark.dart new file mode 100644 index 0000000000..2ce83023c2 --- /dev/null +++ b/pkgs/graphs/benchmark/connected_components_benchmark.dart @@ -0,0 +1,49 @@ +// 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. + +import 'dart:collection'; +import 'dart:math' show Random; + +import 'package:graphs/graphs.dart'; + +void main() { + final _rnd = Random(0); + final size = 2000; + final graph = HashMap>(); + + for (var i = 0; i < size * 3; i++) { + final toList = graph.putIfAbsent(_rnd.nextInt(size), () => []); + + final toValue = _rnd.nextInt(size); + if (!toList.contains(toValue)) { + toList.add(toValue); + } + } + + var maxCount = 0; + var maxIteration = 0; + + final duration = const Duration(milliseconds: 100); + + for (var i = 1;; i++) { + var count = 0; + final watch = Stopwatch()..start(); + while (watch.elapsed < duration) { + count++; + final length = stronglyConnectedComponents( + graph.keys, (v) => v, (e) => graph[e] ?? []).length; + assert(length == 244, '$length'); + } + + if (count > maxCount) { + maxCount = count; + maxIteration = i; + } + + if (maxIteration == i || (i - maxIteration) % 20 == 0) { + print('max iterations in ${duration.inMilliseconds}ms: $maxCount\t' + 'after $maxIteration of $i iterations'); + } + } +} From 1aa0ad55e15e5a0130e0760e9511309175122a82 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Wed, 7 Nov 2018 21:59:42 -0800 Subject: [PATCH 38/88] Breaking: change use optional equals/hashCode instead of keys ...in shortestPath(s) --- pkgs/graphs/CHANGELOG.md | 11 +- pkgs/graphs/README.md | 17 +- .../connected_components_benchmark.dart | 4 +- .../benchmark/shortest_path_benchmark.dart | 5 +- pkgs/graphs/example/example.dart | 5 +- pkgs/graphs/lib/src/crawl_async.dart | 18 +- pkgs/graphs/lib/src/shortest_path.dart | 96 ++++++++--- .../src/strongly_connected_components.dart | 73 ++++---- pkgs/graphs/pubspec.yaml | 2 +- pkgs/graphs/test/crawl_async_test.dart | 6 +- pkgs/graphs/test/shortest_path_test.dart | 91 +++++----- .../strongly_connected_components_test.dart | 162 +++++++++++++++++- pkgs/graphs/test/utils/graph.dart | 32 +++- pkgs/graphs/test/utils/utils.dart | 6 +- 14 files changed, 373 insertions(+), 155 deletions(-) diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index 79b44a9ae0..78722c468b 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,3 +1,12 @@ +# 0.2.0-dev + +- **BREAKING** `shortestPath`, `shortestPaths` and `stronglyConnectedComponents` + now have one generic parameter and have replaced the `key` parameter with + optional params: `{bool equals(T key1, T key2), int hashCode(T key)}`. + This follows the pattern used in `dart:collection` classes `HashMap` and + `LinkedHashMap`. It improves the usability and performance of the case where + the source values are directly usable in a hash data structure. + # 0.1.3+1 - Fixed a bug with non-identity `key` in `shortestPath` and `shortestPaths`. @@ -5,7 +14,7 @@ # 0.1.3 - Added `shortestPath` and `shortestPaths` functions. -- Use `HashMap` and `HashSet` from `dart:collection` for +- Use `HashMap` and `HashSet` from `dart:collection` for `stronglyConnectedComponents`. Improves runtime performance. # 0.1.2+1 diff --git a/pkgs/graphs/README.md b/pkgs/graphs/README.md index c54e615a5e..0edaf92dd5 100644 --- a/pkgs/graphs/README.md +++ b/pkgs/graphs/README.md @@ -21,24 +21,21 @@ class Graph { Node root; } class Node { - List children; + List edges; // Interesting data } ``` Any representation can be adapted to the needs of the algorithm: -- Some algorithms need to associate data with each node in the graph and it will - be keyed by some type `K` that must work as a key in a `HashMap`. If nodes - implement `hashCode` and `==`, or if they are known to have one instance per - logical node such that instance equality is sufficient, then the node can be - passed through directly. - - `(node) => node` - - `(node) => node.id` -- Algorithms which need to traverse the graph take a `children` function which +- Some algorithms need to associate data with each node in the graph. If the + node type `T` does not correctly or efficiently implement `hashCode` or `==`, + you may provide optional `equals` and/or `hashCode` functions are parameters. +- Algorithms which need to traverse the graph take a `edges` function which provides the reachable nodes. - `(node) => graph[node]` - - `(node) => node.children` + - `(node) => node.edges` + Graphs which are resolved asynchronously will have similar functions which return `FutureOr`. diff --git a/pkgs/graphs/benchmark/connected_components_benchmark.dart b/pkgs/graphs/benchmark/connected_components_benchmark.dart index 2ce83023c2..c1c83d7820 100644 --- a/pkgs/graphs/benchmark/connected_components_benchmark.dart +++ b/pkgs/graphs/benchmark/connected_components_benchmark.dart @@ -31,8 +31,8 @@ void main() { final watch = Stopwatch()..start(); while (watch.elapsed < duration) { count++; - final length = stronglyConnectedComponents( - graph.keys, (v) => v, (e) => graph[e] ?? []).length; + final length = + stronglyConnectedComponents(graph.keys, (e) => graph[e] ?? []).length; assert(length == 244, '$length'); } diff --git a/pkgs/graphs/benchmark/shortest_path_benchmark.dart b/pkgs/graphs/benchmark/shortest_path_benchmark.dart index 1b2e0de5e6..2143f56bd1 100644 --- a/pkgs/graphs/benchmark/shortest_path_benchmark.dart +++ b/pkgs/graphs/benchmark/shortest_path_benchmark.dart @@ -25,7 +25,7 @@ void main() { var maxIteration = 0; final testOutput = - shortestPath(0, size - 1, (v) => v, (e) => graph[e] ?? []).toString(); + shortestPath(0, size - 1, (e) => graph[e] ?? []).toString(); print(testOutput); assert(testOutput == '[258, 252, 819, 999]'); @@ -36,8 +36,7 @@ void main() { final watch = Stopwatch()..start(); while (watch.elapsed < duration) { count++; - final length = - shortestPath(0, size - 1, (v) => v, (e) => graph[e] ?? []).length; + final length = shortestPath(0, size - 1, (e) => graph[e] ?? []).length; assert(length == 4, '$length'); } diff --git a/pkgs/graphs/example/example.dart b/pkgs/graphs/example/example.dart index 743b716785..47aca6177d 100644 --- a/pkgs/graphs/example/example.dart +++ b/pkgs/graphs/example/example.dart @@ -9,6 +9,7 @@ import 'package:graphs/graphs.dart'; /// Data is stored on the [Node] class. class Graph { final Map> nodes; + Graph(this.nodes); } @@ -39,8 +40,8 @@ void main() { nodeC: [nodeB, nodeD] }); - var components = stronglyConnectedComponents( - graph.nodes.keys, (node) => node, (node) => graph.nodes[node]); + var components = stronglyConnectedComponents( + graph.nodes.keys, (node) => graph.nodes[node]); print(components); } diff --git a/pkgs/graphs/lib/src/crawl_async.dart b/pkgs/graphs/lib/src/crawl_async.dart index d15d56a225..319a61e413 100644 --- a/pkgs/graphs/lib/src/crawl_async.dart +++ b/pkgs/graphs/lib/src/crawl_async.dart @@ -10,12 +10,12 @@ final _empty = Future.value(null); /// Finds and returns every node in a graph who's nodes and edges are /// asynchronously resolved. /// -/// Cycles are allowed. If this is an undirected graph the [children] function +/// Cycles are allowed. If this is an undirected graph the [edges] function /// may be symmetric. In this case the [roots] may be any node in each connected /// graph. /// /// [V] is the type of values in the graph nodes. [K] must be a type suitable -/// for using as a Map or Set key. [children] should return the next reachable +/// for using as a Map or Set key. [edges] should return the next reachable /// nodes. /// /// There are no ordering guarantees. This is useful for ensuring some work is @@ -26,12 +26,12 @@ final _empty = Future.value(null); /// the graph. If missing nodes are important they should be tracked within the /// [readNode] callback. /// -/// If either [readNode] or [children] throws the error will be forwarded +/// If either [readNode] or [edges] throws the error will be forwarded /// through the result stream and no further nodes will be crawled, though some /// work may have already been started. Stream crawlAsync(Iterable roots, FutureOr Function(K) readNode, - FutureOr> Function(K, V) children) { - final crawl = _CrawlAsync(roots, readNode, children)..run(); + FutureOr> Function(K, V) edges) { + final crawl = _CrawlAsync(roots, readNode, edges)..run(); return crawl.result.stream; } @@ -39,12 +39,12 @@ class _CrawlAsync { final result = StreamController(); final FutureOr Function(K) readNode; - final FutureOr> Function(K, V) children; + final FutureOr> Function(K, V) edges; final Iterable roots; final _seen = HashSet(); - _CrawlAsync(this.roots, this.readNode, this.children); + _CrawlAsync(this.roots, this.readNode, this.edges); /// Add all nodes in the graph to [result] and return a Future which fires /// after all nodes have been seen. @@ -59,13 +59,13 @@ class _CrawlAsync { } /// Resolve the node at [key] and output it, then start crawling all of it's - /// children. + /// edges. Future _crawlFrom(K key) async { var value = await readNode(key); if (value == null) return; if (result.isClosed) return; result.add(value); - var next = await children(key, value) ?? const []; + var next = await edges(key, value) ?? const []; await Future.wait(next.map(_visit), eagerError: true); } diff --git a/pkgs/graphs/lib/src/shortest_path.dart b/pkgs/graphs/lib/src/shortest_path.dart index f8f06d0b4f..b591694aec 100644 --- a/pkgs/graphs/lib/src/shortest_path.dart +++ b/pkgs/graphs/lib/src/shortest_path.dart @@ -10,56 +10,94 @@ import 'dart:collection'; /// If [start] `==` [target], an empty [List] is returned and [edges] is never /// called. /// -/// [V] is the type of values in the graph nodes. [K] must be a type suitable -/// for using as a Map or Set key, and [key] must provide a consistent key for -/// every node. -/// /// [start], [target] and all values returned by [edges] must not be `null`. /// If asserts are enabled, an [AssertionError] is raised if these conditions /// are not met. If asserts are not enabled, violations result in undefined /// behavior. -List shortestPath( - V start, V target, K Function(V) key, Iterable Function(V) edges) => - _shortestPaths(start, key, edges, key(target))[key(target)]; +/// +/// If [equals] is provided, it is used to compare nodes in the graph. If +/// [equals] is omitted, the node's own [Object.==] is used instead. +/// +/// Similarly, if [hashCode] is provided, it is used to produce a hash value +/// for nodes to efficiently calculate the return value. If it is omitted, the +/// key's own [Object.hashCode] is used. +/// +/// If you supply one of [equals] or [hashCode], you should generally also to +/// supply the other. +List shortestPath( + T start, + T target, + Iterable Function(T) edges, { + bool equals(T key1, T key2), + int hashCode(T key), +}) => + _shortestPaths( + start, + edges, + target: target, + equals: equals, + hashCode: hashCode, + )[target]; /// Returns a [Map] of the shortest paths from [start] to all of the nodes in /// the directed graph defined by [edges]. /// /// All return values will contain the key [start] with an empty [List] value. /// -/// [V] is the type of values in the graph nodes. [K] must be a type suitable -/// for using as a Map or Set key, and [key] must provide a consistent key for -/// every node. -/// /// [start] and all values returned by [edges] must not be `null`. /// If asserts are enabled, an [AssertionError] is raised if these conditions /// are not met. If asserts are not enabled, violations result in undefined /// behavior. -Map> shortestPaths( - V start, K Function(V) key, Iterable Function(V) edges) => - _shortestPaths(start, key, edges); +/// +/// If [equals] is provided, it is used to compare nodes in the graph. If +/// [equals] is omitted, the node's own [Object.==] is used instead. +/// +/// Similarly, if [hashCode] is provided, it is used to produce a hash value +/// for nodes to efficiently calculate the return value. If it is omitted, the +/// key's own [Object.hashCode] is used. +/// +/// If you supply one of [equals] or [hashCode], you should generally also to +/// supply the other. +Map> shortestPaths( + T start, + Iterable Function(T) edges, { + bool equals(T key1, T key2), + int hashCode(T key), +}) => + _shortestPaths( + start, + edges, + equals: equals, + hashCode: hashCode, + ); -Map> _shortestPaths( - V start, K Function(V) key, Iterable Function(V) edges, - [K targetKey]) { +Map> _shortestPaths( + T start, + Iterable Function(T) edges, { + T target, + bool equals(T key1, T key2), + int hashCode(T key), + bool isValidKey(potentialKey), +}) { assert(start != null, '`start` cannot be null'); - assert(key != null, '`key` cannot be null`.'); assert(edges != null, '`edges` cannot be null'); - final distances = HashMap>(); - distances[key(start)] = []; + final distances = HashMap>( + equals: equals, hashCode: hashCode, isValidKey: isValidKey); + distances[start] = []; - if (key(start) == targetKey) { + equals ??= _defaultEquals; + if (equals(start, target)) { return distances; } - final toVisit = ListQueue()..add(start); + final toVisit = ListQueue()..add(start); - List bestOption; + List bestOption; while (toVisit.isNotEmpty) { final current = toVisit.removeFirst(); - final distanceToCurrent = distances[key(current)]; + final distanceToCurrent = distances[current]; if (bestOption != null && distanceToCurrent.length >= bestOption.length) { // Skip any existing `toVisit` items that have no chance of being @@ -69,18 +107,18 @@ Map> _shortestPaths( for (var edge in edges(current)) { assert(edge != null, '`edges` cannot return null values.'); - final existingPath = distances[key(edge)]; + final existingPath = distances[edge]; if (existingPath == null || existingPath.length > (distanceToCurrent.length + 1)) { - final newOption = distanceToCurrent.followedBy([edge]).toList(); + final newOption = distanceToCurrent.followedBy([edge]).toList(); - if (key(edge) == targetKey) { + if (equals(edge, target)) { assert(bestOption == null || bestOption.length > newOption.length); bestOption = newOption; } - distances[key(edge)] = newOption; + distances[edge] = newOption; if (bestOption == null || bestOption.length > newOption.length) { // Only add a node to visit if it might be a better path to the // target node @@ -92,3 +130,5 @@ Map> _shortestPaths( return distances; } + +bool _defaultEquals(a, b) => a == b; diff --git a/pkgs/graphs/lib/src/strongly_connected_components.dart b/pkgs/graphs/lib/src/strongly_connected_components.dart index 62cb6147ef..19519a8b61 100644 --- a/pkgs/graphs/lib/src/strongly_connected_components.dart +++ b/pkgs/graphs/lib/src/strongly_connected_components.dart @@ -10,61 +10,72 @@ import 'dart:math' show min; /// /// The result will be a valid reverse topological order ordering of the /// strongly connected components. Components further from a root will appear in -/// the result before the components which they are children of. +/// the result before the components which they are connected to. /// /// Nodes within a strongly connected component have no ordering guarantees, /// except that if the first value in [nodes] is a valid root, and is contained /// in a cycle, it will be the last element of that cycle. /// -/// [V] is the type of values in the graph nodes. [K] must be a type suitable -/// for using as a Map or Set key, and [key] must provide a consistent key for -/// every node. [children] should return the next reachable nodes. -/// /// [nodes] must contain at least a root of every tree in the graph if there are /// disjoint subgraphs but it may contain all nodes in the graph if the roots /// are not known. -List> stronglyConnectedComponents( - Iterable nodes, K Function(V) key, Iterable Function(V) children) { - final result = >[]; - final lowLinks = HashMap(); - final indexes = HashMap(); - final onStack = HashSet(); +/// +/// If [equals] is provided, it is used to compare nodes in the graph. If +/// [equals] is omitted, the node's own [Object.==] is used instead. +/// +/// Similarly, if [hashCode] is provided, it is used to produce a hash value +/// for nodes to efficiently calculate the return value. If it is omitted, the +/// key's own [Object.hashCode] is used. +/// +/// If you supply one of [equals] or [hashCode], you should generally also to +/// supply the other. +List> stronglyConnectedComponents( + Iterable nodes, + Iterable Function(T) edges, { + bool equals(T key1, T key2), + int hashCode(T key), +}) { + final result = >[]; + final lowLinks = HashMap(equals: equals, hashCode: hashCode); + final indexes = HashMap(equals: equals, hashCode: hashCode); + final onStack = HashSet(equals: equals, hashCode: hashCode); + + equals ??= _defaultEquals; var index = 0; - var lastVisited = Queue(); + var lastVisited = Queue(); - void strongConnect(V node) { - var nodeKey = key(node); - indexes[nodeKey] = index; - lowLinks[nodeKey] = index; + void strongConnect(T node) { + indexes[node] = index; + lowLinks[node] = index; index++; lastVisited.addLast(node); - onStack.add(nodeKey); + onStack.add(node); // ignore: omit_local_variable_types - for (final V next in children(node) ?? const []) { - var nextKey = key(next); - if (!indexes.containsKey(nextKey)) { + for (final T next in edges(node) ?? const []) { + if (!indexes.containsKey(next)) { strongConnect(next); - lowLinks[nodeKey] = min(lowLinks[nodeKey], lowLinks[nextKey]); - } else if (onStack.contains(nextKey)) { - lowLinks[nodeKey] = min(lowLinks[nodeKey], indexes[nextKey]); + lowLinks[node] = min(lowLinks[node], lowLinks[next]); + } else if (onStack.contains(next)) { + lowLinks[node] = min(lowLinks[node], indexes[next]); } } - if (lowLinks[nodeKey] == indexes[nodeKey]) { - final component = []; - K nextKey; + if (lowLinks[node] == indexes[node]) { + final component = []; + T next; do { - var next = lastVisited.removeLast(); - nextKey = key(next); - onStack.remove(nextKey); + next = lastVisited.removeLast(); + onStack.remove(next); component.add(next); - } while (nextKey != nodeKey); + } while (!equals(next, node)); result.add(component); } } for (final node in nodes) { - if (!indexes.containsKey(key(node))) strongConnect(node); + if (!indexes.containsKey(node)) strongConnect(node); } return result; } + +bool _defaultEquals(a, b) => a == b; diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 0caee34057..5840fcb454 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,5 +1,5 @@ name: graphs -version: 0.1.3+1 +version: 0.2.0-dev description: Graph algorithms operation an graphs in any representation. author: Dart Team homepage: https://github.com/dart-lang/graphs diff --git a/pkgs/graphs/test/crawl_async_test.dart b/pkgs/graphs/test/crawl_async_test.dart index 64b4dbd0f1..892a0bd5d8 100644 --- a/pkgs/graphs/test/crawl_async_test.dart +++ b/pkgs/graphs/test/crawl_async_test.dart @@ -15,7 +15,7 @@ void main() { Future> crawl( Map> g, Iterable roots) { var graph = AsyncGraph(g); - return crawlAsync(roots, graph.readNode, graph.children).toList(); + return crawlAsync(roots, graph.readNode, graph.edges).toList(); } test('empty result for empty graph', () async { @@ -65,7 +65,7 @@ void main() { expect(result, allOf(contains('a'), contains('b'))); }); - test('allows null children', () async { + test('allows null edges', () async { var result = await crawl({ 'a': ['b'], 'b': null, @@ -85,7 +85,7 @@ void main() { expect(result, ['a']); }); - test('surfaces exceptions for crawling children', () { + test('surfaces exceptions for crawling edges', () { var graph = { 'a': ['b'], }; diff --git a/pkgs/graphs/test/shortest_path_test.dart b/pkgs/graphs/test/shortest_path_test.dart index 253296b5e2..4a09f94510 100644 --- a/pkgs/graphs/test/shortest_path_test.dart +++ b/pkgs/graphs/test/shortest_path_test.dart @@ -11,79 +11,76 @@ Matcher _throwsAssertionError(messageMatcher) => throwsA(const TypeMatcher() .having((ae) => ae.message, 'message', messageMatcher)); -int _xKey(X input) => input.value; - -T _identity(T input) => input; - void main() { - const graph = >{ - 1: [2, 5], - 2: [3], - 3: [4, 5], - 4: [1], - 5: [8], - 6: [7], + const graph = >{ + '1': ['2', '5'], + '2': ['3'], + '3': ['4', '5'], + '4': ['1'], + '5': ['8'], + '6': ['7'], }; - List getValues(int key) => graph[key] ?? []; + List getValues(String key) => graph[key] ?? []; List getXValues(X key) => graph[key.value]?.map((v) => X(v))?.toList() ?? []; test('null `start` throws AssertionError', () { - expect(() => shortestPath(null, 1, _identity, getValues), + expect(() => shortestPath(null, '1', getValues), _throwsAssertionError('`start` cannot be null')); - expect(() => shortestPaths(null, _identity, getValues), + expect(() => shortestPaths(null, getValues), _throwsAssertionError('`start` cannot be null')); }); test('null `edges` throws AssertionError', () { - expect(() => shortestPath(1, 1, _identity, null), + expect(() => shortestPath(1, 1, null), _throwsAssertionError('`edges` cannot be null')); - expect(() => shortestPaths(1, _identity, null), + expect(() => shortestPaths(1, null), _throwsAssertionError('`edges` cannot be null')); }); test('null return value from `edges` throws', () { - expect(shortestPath(1, 1, _identity, (input) => null), [], + expect(shortestPath(1, 1, (input) => null), [], reason: 'self target short-circuits'); - expect(shortestPath(1, 1, _identity, (input) => [null]), [], + expect(shortestPath(1, 1, (input) => [null]), [], reason: 'self target short-circuits'); - expect(() => shortestPath(1, 2, _identity, (input) => null), - throwsNoSuchMethodError); + expect(() => shortestPath(1, 2, (input) => null), throwsNoSuchMethodError); - expect(() => shortestPaths(1, _identity, (input) => null), - throwsNoSuchMethodError); + expect(() => shortestPaths(1, (input) => null), throwsNoSuchMethodError); - expect(() => shortestPath(1, 2, _identity, (input) => [null]), + expect(() => shortestPath(1, 2, (input) => [null]), _throwsAssertionError('`edges` cannot return null values.')); - expect(() => shortestPaths(1, _identity, (input) => [null]), + expect(() => shortestPaths(1, (input) => [null]), _throwsAssertionError('`edges` cannot return null values.')); }); - void _singlePathTest(int from, int to, List expected) { + void _singlePathTest(String from, String to, List expected) { test('$from -> $to should be $expected (mapped)', () { expect( - shortestPath(X(from), X(to), _xKey, getXValues) + shortestPath(X(from), X(to), getXValues, + equals: xEquals, hashCode: xHashCode) ?.map((x) => x.value), expected); }); test('$from -> $to should be $expected', () { - expect(shortestPath(from, to, _identity, getValues), expected); + expect(shortestPath(from, to, getValues), expected); }); } - void _pathsTest(int from, Map> expected, List nullPaths) { + void _pathsTest( + String from, Map> expected, List nullPaths) { test('paths from $from (mapped)', () { - final result = shortestPaths(X(from), _xKey, getXValues) - .map((k, v) => MapEntry(k, v.map((x) => x.value).toList())); + final result = shortestPaths(X(from), getXValues, + equals: xEquals, hashCode: xHashCode) + .map((k, v) => MapEntry(k.value, v.map((x) => x.value).toList())); expect(result, expected); }); test('paths from $from', () { - final result = shortestPaths(from, _identity, getValues); + final result = shortestPaths(from, getValues); expect(result, expected); }); @@ -96,27 +93,27 @@ void main() { } } - _pathsTest(1, { - 5: [5], - 3: [2, 3], - 8: [5, 8], - 1: [], - 2: [2], - 4: [2, 3, 4], + _pathsTest('1', { + '5': ['5'], + '3': ['2', '3'], + '8': ['5', '8'], + '1': [], + '2': ['2'], + '4': ['2', '3', '4'], }, [ - 6, - 7, + '6', + '7', ]); - _pathsTest(6, { - 7: [7], - 6: [], + _pathsTest('6', { + '7': ['7'], + '6': [], }, [ - 1, + '1', ]); - _pathsTest(7, {7: []}, [1, 6]); + _pathsTest('7', {'7': []}, ['1', '6']); - _pathsTest(42, {42: []}, [1, 6]); + _pathsTest('42', {'42': []}, ['1', '6']); - _singlePathTest(1, null, null); + _singlePathTest('1', null, null); } diff --git a/pkgs/graphs/test/strongly_connected_components_test.dart b/pkgs/graphs/test/strongly_connected_components_test.dart index 0e2d97770e..6c3968d449 100644 --- a/pkgs/graphs/test/strongly_connected_components_test.dart +++ b/pkgs/graphs/test/strongly_connected_components_test.dart @@ -2,18 +2,22 @@ // 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. -import 'package:test/test.dart'; import 'package:graphs/graphs.dart'; +import 'package:test/test.dart'; import 'utils/graph.dart'; +import 'utils/utils.dart'; void main() { group('strongly connected components', () { /// Run [stronglyConnectedComponents] on [g]. - List> components(Map> g) { + List> components( + Map> g, { + Iterable startNodes, + }) { final graph = Graph(g); return stronglyConnectedComponents( - graph.allNodes, graph.key, graph.children); + startNodes ?? graph.allNodes, graph.edges); } test('empty result for empty graph', () { @@ -53,14 +57,154 @@ void main() { test('includes the first passed root last in a cycle', () { // In cases where this is used to find a topological ordering the first // value in nodes should always come last. - var graph = Graph({ + var graph = { 'a': ['b'], 'b': ['a'] + }; + var resultFromA = components(graph, startNodes: ['a']); + var resultFromB = components(graph, startNodes: ['b']); + expect(resultFromA.single.last, 'a'); + expect(resultFromB.single.last, 'b'); + }); + + test('handles cycles in the middle', () { + var result = components({ + 'a': ['b', 'c'], + 'b': ['c', 'd'], + 'c': ['b', 'd'], + 'd': [], + }); + expect(result, [ + ['d'], + allOf(contains('b'), contains('c')), + ['a'], + ]); + }); + + test('handles self cycles', () { + var result = components({ + 'a': ['b'], + 'b': ['b'], + }); + expect(result, [ + ['b'], + ['a'], + ]); + }); + + test('valid topological ordering for disjoint subgraphs', () { + var result = components({ + 'a': ['b', 'c'], + 'b': ['b1', 'b2'], + 'c': ['c1', 'c2'], + 'b1': [], + 'b2': [], + 'c1': [], + 'c2': [] + }); + + expect( + result, + containsAllInOrder([ + ['c1'], + ['c'], + ['a'] + ])); + expect( + result, + containsAllInOrder([ + ['c2'], + ['c'], + ['a'] + ])); + expect( + result, + containsAllInOrder([ + ['b1'], + ['b'], + ['a'] + ])); + expect( + result, + containsAllInOrder([ + ['b2'], + ['b'], + ['a'] + ])); + }); + + test('handles getting null for edges', () { + var result = components({ + 'a': ['b'], + 'b': null, }); - var resultFromA = - stronglyConnectedComponents(['a'], graph.key, graph.children); - var resultFromB = - stronglyConnectedComponents(['b'], graph.key, graph.children); + expect(result, [ + ['b'], + ['a'] + ]); + }); + }); + + group('custom hashCode and equals', () { + /// Run [stronglyConnectedComponents] on [g]. + List> components( + Map> g, { + Iterable startNodes, + }) { + final graph = BadGraph(g); + + startNodes ??= graph.allNodes.map((n) => n.value); + + return stronglyConnectedComponents( + startNodes.map((n) => X(n)), graph.edges, + equals: xEquals, hashCode: xHashCode) + .map((list) => list.map((x) => x.value).toList()) + .toList(); + } + + test('empty result for empty graph', () { + var result = components({}); + expect(result, isEmpty); + }); + + test('single item for single node', () { + var result = components({'a': []}); + expect(result, [ + ['a'] + ]); + }); + + test('handles non-cycles', () { + var result = components({ + 'a': ['b'], + 'b': ['c'], + 'c': [] + }); + expect(result, [ + ['c'], + ['b'], + ['a'] + ]); + }); + + test('handles entire graph as cycle', () { + var result = components({ + 'a': ['b'], + 'b': ['c'], + 'c': ['a'] + }); + expect(result, [allOf(contains('a'), contains('b'), contains('c'))]); + }); + + test('includes the first passed root last in a cycle', () { + // In cases where this is used to find a topological ordering the first + // value in nodes should always come last. + var graph = { + 'a': ['b'], + 'b': ['a'] + }; + var resultFromA = components(graph, startNodes: ['a']); + var resultFromB = components(graph, startNodes: ['b']); expect(resultFromA.single.last, 'a'); expect(resultFromB.single.last, 'b'); }); @@ -131,7 +275,7 @@ void main() { ])); }); - test('handles getting null for children', () { + test('handles getting null for edges', () { var result = components({ 'a': ['b'], 'b': null, diff --git a/pkgs/graphs/test/utils/graph.dart b/pkgs/graphs/test/utils/graph.dart index a4ff65c59b..29988abcec 100644 --- a/pkgs/graphs/test/utils/graph.dart +++ b/pkgs/graphs/test/utils/graph.dart @@ -3,20 +3,36 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:collection'; + +import 'utils.dart'; /// A representation of a Graph since none is specified in `lib/`. class Graph { - final Map> graph; + final Map> _graph; + + Graph(this._graph); - Graph(this.graph); + List edges(String node) => _graph[node]; - String key(String node) => node; - List children(String node) => graph[node]; - Iterable get allNodes => graph.keys; + Iterable get allNodes => _graph.keys; +} + +class BadGraph { + final Map> _graph; + + BadGraph(Map> values) + : _graph = LinkedHashMap(equals: xEquals, hashCode: xHashCode) + ..addEntries(values.entries.map( + (e) => MapEntry(X(e.key), e?.value?.map((v) => X(v))?.toList()))); + + List edges(X node) => _graph[node]; + + Iterable get allNodes => _graph.keys; } /// A representation of a Graph where keys can asynchronously be resolved to -/// real values or to children. +/// real values or to edges. class AsyncGraph { final Map> graph; @@ -24,6 +40,6 @@ class AsyncGraph { Future readNode(String node) async => graph.containsKey(node) ? node : null; - Future> children(String key, String node) async => - graph[key]; + + Future> edges(String key, String node) async => graph[key]; } diff --git a/pkgs/graphs/test/utils/utils.dart b/pkgs/graphs/test/utils/utils.dart index 828b91ebf7..f4ed52b37d 100644 --- a/pkgs/graphs/test/utils/utils.dart +++ b/pkgs/graphs/test/utils/utils.dart @@ -2,8 +2,12 @@ // 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. +bool xEquals(X a, X b) => a?.value == b?.value; + +int xHashCode(X a) => a.value.hashCode; + class X { - final int value; + final String value; X(this.value); From 10918ff684be2979ea62e94b63a9d3acf8b09ff1 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 13 Nov 2018 10:52:52 -0800 Subject: [PATCH 39/88] shortestPath: remove unused isValidKey param (#32) --- pkgs/graphs/lib/src/shortest_path.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkgs/graphs/lib/src/shortest_path.dart b/pkgs/graphs/lib/src/shortest_path.dart index b591694aec..4fb78a1c56 100644 --- a/pkgs/graphs/lib/src/shortest_path.dart +++ b/pkgs/graphs/lib/src/shortest_path.dart @@ -77,13 +77,11 @@ Map> _shortestPaths( T target, bool equals(T key1, T key2), int hashCode(T key), - bool isValidKey(potentialKey), }) { assert(start != null, '`start` cannot be null'); assert(edges != null, '`edges` cannot be null'); - final distances = HashMap>( - equals: equals, hashCode: hashCode, isValidKey: isValidKey); + final distances = HashMap>(equals: equals, hashCode: hashCode); distances[start] = []; equals ??= _defaultEquals; From 92d2a378110613befb8ccaa27f0eeb65b03b43bf Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 13 Nov 2018 12:27:11 -0800 Subject: [PATCH 40/88] shortedPath(s): use fixed length List where possible iterations/ms in benchmark 1174 -> 1368: 17% improvement --- pkgs/graphs/lib/src/shortest_path.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pkgs/graphs/lib/src/shortest_path.dart b/pkgs/graphs/lib/src/shortest_path.dart index 4fb78a1c56..b4c969fe64 100644 --- a/pkgs/graphs/lib/src/shortest_path.dart +++ b/pkgs/graphs/lib/src/shortest_path.dart @@ -82,7 +82,7 @@ Map> _shortestPaths( assert(edges != null, '`edges` cannot be null'); final distances = HashMap>(equals: equals, hashCode: hashCode); - distances[start] = []; + distances[start] = List(0); equals ??= _defaultEquals; if (equals(start, target)) { @@ -95,9 +95,10 @@ Map> _shortestPaths( while (toVisit.isNotEmpty) { final current = toVisit.removeFirst(); - final distanceToCurrent = distances[current]; + final currentPath = distances[current]; + final currentPathLength = currentPath.length; - if (bestOption != null && distanceToCurrent.length >= bestOption.length) { + if (bestOption != null && currentPathLength >= bestOption.length) { // Skip any existing `toVisit` items that have no chance of being // better than bestOption (if it exists) continue; @@ -108,8 +109,10 @@ Map> _shortestPaths( final existingPath = distances[edge]; if (existingPath == null || - existingPath.length > (distanceToCurrent.length + 1)) { - final newOption = distanceToCurrent.followedBy([edge]).toList(); + existingPath.length > (currentPathLength + 1)) { + final newOption = List(currentPathLength + 1) + ..setAll(0, currentPath) + ..[currentPathLength] = edge; if (equals(edge, target)) { assert(bestOption == null || bestOption.length > newOption.length); From 239d259b6b0f9224d5242db319bcb9c0ae4ecdb4 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 13 Nov 2018 13:11:40 -0800 Subject: [PATCH 41/88] Improve short-circuit logic iterations/ms in benchmark 1368 -> 1784: 30% improvement --- pkgs/graphs/lib/src/shortest_path.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkgs/graphs/lib/src/shortest_path.dart b/pkgs/graphs/lib/src/shortest_path.dart index b4c969fe64..530793cb86 100644 --- a/pkgs/graphs/lib/src/shortest_path.dart +++ b/pkgs/graphs/lib/src/shortest_path.dart @@ -98,7 +98,7 @@ Map> _shortestPaths( final currentPath = distances[current]; final currentPathLength = currentPath.length; - if (bestOption != null && currentPathLength >= bestOption.length) { + if (bestOption != null && (currentPathLength + 1) >= bestOption.length) { // Skip any existing `toVisit` items that have no chance of being // better than bestOption (if it exists) continue; @@ -108,8 +108,10 @@ Map> _shortestPaths( assert(edge != null, '`edges` cannot return null values.'); final existingPath = distances[edge]; - if (existingPath == null || - existingPath.length > (currentPathLength + 1)) { + assert(existingPath == null || + existingPath.length <= (currentPathLength + 1)); + + if (existingPath == null) { final newOption = List(currentPathLength + 1) ..setAll(0, currentPath) ..[currentPathLength] = edge; From b52673263d6a11bcfbde2bd11815db977fad805c Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 13 Nov 2018 15:08:49 -0800 Subject: [PATCH 42/88] Use setRange over setAll in fixed-length list (#34) benchmark iterations/ms 1770 -> 2401: 36% faster --- pkgs/graphs/lib/src/shortest_path.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/graphs/lib/src/shortest_path.dart b/pkgs/graphs/lib/src/shortest_path.dart index 530793cb86..f295e5d850 100644 --- a/pkgs/graphs/lib/src/shortest_path.dart +++ b/pkgs/graphs/lib/src/shortest_path.dart @@ -113,7 +113,7 @@ Map> _shortestPaths( if (existingPath == null) { final newOption = List(currentPathLength + 1) - ..setAll(0, currentPath) + ..setRange(0, currentPathLength, currentPath) ..[currentPathLength] = edge; if (equals(edge, target)) { From 5609bc58507f8ce6cfafd0e61c4a5c28e68b04f7 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 11 Dec 2018 12:39:42 -0800 Subject: [PATCH 43/88] shortestPath: add an integration test that validates invariants (#37) Helps to ensure future optimizations don't unintentionally break things --- pkgs/graphs/test/shortest_path_test.dart | 68 ++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/pkgs/graphs/test/shortest_path_test.dart b/pkgs/graphs/test/shortest_path_test.dart index 4a09f94510..f64da09861 100644 --- a/pkgs/graphs/test/shortest_path_test.dart +++ b/pkgs/graphs/test/shortest_path_test.dart @@ -2,6 +2,9 @@ // 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. +import 'dart:collection'; +import 'dart:math' show Random; + import 'package:graphs/graphs.dart'; import 'package:test/test.dart'; @@ -116,4 +119,69 @@ void main() { _pathsTest('42', {'42': []}, ['1', '6']); _singlePathTest('1', null, null); + + test('integration test', () { + // Be deterministic in the generated graph. This test may have to be updated + // if the behavior of `Random` changes for the provided seed. + final _rnd = Random(1); + final size = 1000; + final graph = HashMap>(); + + List resultForGraph() => + shortestPath(0, size - 1, (e) => graph[e] ?? const []); + + void addRandomEdge() { + final toList = graph.putIfAbsent(_rnd.nextInt(size), () => []); + + final toValue = _rnd.nextInt(size); + if (!toList.contains(toValue)) { + toList.add(toValue); + } + } + + List result; + + // Add edges until there is a shortest path between `0` and `size - 1` + do { + addRandomEdge(); + result = resultForGraph(); + } while (result == null); + + expect(result, [313, 547, 91, 481, 74, 64, 439, 388, 660, 275, 999]); + + var count = 0; + // Add edges until the shortest path between `0` and `size - 1` is 2 items + // Adding edges should never increase the length of the shortest path. + // Adding enough edges should reduce the length of the shortest path. + do { + expect(++count, lessThan(size * 5), reason: 'This loop should finish.'); + addRandomEdge(); + final previousResultLength = result.length; + result = resultForGraph(); + expect(result, hasLength(lessThanOrEqualTo(previousResultLength))); + } while (result.length > 2); + + expect(result, [275, 999]); + + count = 0; + // Remove edges until there is no shortest path. + // Removing edges should never reduce the length of the shortest path. + // Removing enough edges should increase the length of the shortest path and + // eventually eliminate any path. + do { + expect(++count, lessThan(size * 5), reason: 'This loop should finish.'); + final randomKey = graph.keys.elementAt(_rnd.nextInt(graph.length)); + final list = graph[randomKey]; + expect(list, isNotEmpty); + list.removeAt(_rnd.nextInt(list.length)); + if (list.isEmpty) { + graph.remove(randomKey); + } + final previousResultLength = result.length; + result = resultForGraph(); + if (result != null) { + expect(result, hasLength(greaterThanOrEqualTo(previousResultLength))); + } + } while (result != null); + }); } From 50455b3c26b86906e5663999e8d77f21295d5133 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 11 Dec 2018 12:41:07 -0800 Subject: [PATCH 44/88] shortestPath: update benchmark to report fastest iteration time (#36) Makes it easier to compare peak performance between changes --- .../benchmark/shortest_path_benchmark.dart | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/pkgs/graphs/benchmark/shortest_path_benchmark.dart b/pkgs/graphs/benchmark/shortest_path_benchmark.dart index 2143f56bd1..813c18b04f 100644 --- a/pkgs/graphs/benchmark/shortest_path_benchmark.dart +++ b/pkgs/graphs/benchmark/shortest_path_benchmark.dart @@ -21,7 +21,7 @@ void main() { } } - var maxCount = 0; + int minTicks; var maxIteration = 0; final testOutput = @@ -29,24 +29,22 @@ void main() { print(testOutput); assert(testOutput == '[258, 252, 819, 999]'); - final duration = const Duration(milliseconds: 100); - + final watch = Stopwatch(); for (var i = 1;; i++) { - var count = 0; - final watch = Stopwatch()..start(); - while (watch.elapsed < duration) { - count++; - final length = shortestPath(0, size - 1, (e) => graph[e] ?? []).length; - assert(length == 4, '$length'); - } - - if (count > maxCount) { - maxCount = count; + watch + ..reset() + ..start(); + final length = shortestPath(0, size - 1, (e) => graph[e] ?? []).length; + watch.stop(); + assert(length == 4, '$length'); + + if (minTicks == null || watch.elapsedTicks < minTicks) { + minTicks = watch.elapsedTicks; maxIteration = i; } - if (maxIteration == i || (i - maxIteration) % 20 == 0) { - print('max iterations in ${duration.inMilliseconds}ms: $maxCount\t' + if (maxIteration == i || (i - maxIteration) % 100000 == 0) { + print('min ticks for one run: $minTicks\t' 'after $maxIteration of $i iterations'); } } From ded042428a96c122cfe1947aedc6076dea207890 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 11 Dec 2018 12:41:44 -0800 Subject: [PATCH 45/88] Update dev_dep to pkg:analyzer (#35) --- pkgs/graphs/example/crawl_async_example.dart | 2 +- pkgs/graphs/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/example/crawl_async_example.dart b/pkgs/graphs/example/crawl_async_example.dart index 1d4f87774a..959b894cff 100644 --- a/pkgs/graphs/example/crawl_async_example.dart +++ b/pkgs/graphs/example/crawl_async_example.dart @@ -51,7 +51,7 @@ Future> findImports(Uri from, Source source) async { Future parseUri(Uri uri) async { var path = await pathForUri(uri); var analysisSession = (await analysisContext).currentSession; - var parseResult = analysisSession.getParsedAstSync(path); + var parseResult = analysisSession.getParsedUnit(path); return parseResult.unit; } diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 5840fcb454..72b34c0fa5 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -12,5 +12,5 @@ dev_dependencies: test: ^1.5.1 # For examples - analyzer: ^0.33.0 + analyzer: ^0.34.0 path: ^1.1.0 From 4979ead0905514265f2fc3161e973181b9d72bd8 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Wed, 12 Dec 2018 12:08:06 -0800 Subject: [PATCH 46/88] Update description, prepare for 0.2.0 (#38) --- pkgs/graphs/pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 72b34c0fa5..12c5ff4063 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,6 +1,6 @@ name: graphs -version: 0.2.0-dev -description: Graph algorithms operation an graphs in any representation. +version: 0.2.0 +description: Graph algorithms that operate on graphs in any representation author: Dart Team homepage: https://github.com/dart-lang/graphs From 53f9d3b56b8e8760bf43e4cf609d4c0ffb21411f Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Fri, 28 Dec 2018 15:16:22 -0800 Subject: [PATCH 47/88] Clarify that edges may have overlapping calls (#40) Closes #39 Add a note in the doc comment that the edges callback must be throttled independently, it won't be throttled by `crawlAsync`. Update the example usage to show how the `pool` package can be used to implement limiting. --- pkgs/graphs/example/crawl_async_example.dart | 9 +++++++-- pkgs/graphs/lib/src/crawl_async.dart | 4 ++++ pkgs/graphs/pubspec.yaml | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/example/crawl_async_example.dart b/pkgs/graphs/example/crawl_async_example.dart index 959b894cff..ce4c3e39d8 100644 --- a/pkgs/graphs/example/crawl_async_example.dart +++ b/pkgs/graphs/example/crawl_async_example.dart @@ -11,12 +11,17 @@ import 'package:analyzer/dart/analysis/context_locator.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:graphs/graphs.dart'; import 'package:path/path.dart' as p; +import 'package:pool/pool.dart'; /// Print a transitive set of imported URIs where libraries are read /// asynchronously. Future main() async { - var allImports = await crawlAsync( - [Uri.parse('package:graphs/graphs.dart')], read, findImports) + // Limits calls to [findImports]. + var _pool = Pool(10); + var allImports = await crawlAsync( + [Uri.parse('package:graphs/graphs.dart')], + read, + (from, source) => _pool.withResource(() => findImports(from, source))) .toList(); print(allImports.map((s) => s.uri).toList()); } diff --git a/pkgs/graphs/lib/src/crawl_async.dart b/pkgs/graphs/lib/src/crawl_async.dart index 319a61e413..2cf0f54189 100644 --- a/pkgs/graphs/lib/src/crawl_async.dart +++ b/pkgs/graphs/lib/src/crawl_async.dart @@ -29,6 +29,10 @@ final _empty = Future.value(null); /// If either [readNode] or [edges] throws the error will be forwarded /// through the result stream and no further nodes will be crawled, though some /// work may have already been started. +/// +/// Crawling is eager, so calls to [edges] may overlap with other calls that +/// have not completed. If the [edges] callback needs to be limited or throttled +/// that must be done by wrapping it before calling [crawlAsync]. Stream crawlAsync(Iterable roots, FutureOr Function(K) readNode, FutureOr> Function(K, V) edges) { final crawl = _CrawlAsync(roots, readNode, edges)..run(); diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 12c5ff4063..21b796c577 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -14,3 +14,4 @@ dev_dependencies: # For examples analyzer: ^0.34.0 path: ^1.1.0 + pool: ^1.3.0 From 99388aa2df421221e94521ebec2dc158d52ce3e4 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Fri, 1 Feb 2019 11:23:36 -0800 Subject: [PATCH 48/88] bump to latest analyzer (dev dep for examples), fixed duplicate lint (#42) --- pkgs/graphs/analysis_options.yaml | 1 - pkgs/graphs/pubspec.yaml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pkgs/graphs/analysis_options.yaml b/pkgs/graphs/analysis_options.yaml index 6e159f34f8..d6bd4308fc 100644 --- a/pkgs/graphs/analysis_options.yaml +++ b/pkgs/graphs/analysis_options.yaml @@ -51,7 +51,6 @@ linter: - prefer_single_quotes - prefer_typing_uninitialized_variables - slash_for_doc_comments - - test_types_in_equals - super_goes_last - test_types_in_equals - throw_in_finally diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 21b796c577..c4c4871ee4 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -12,6 +12,6 @@ dev_dependencies: test: ^1.5.1 # For examples - analyzer: ^0.34.0 + analyzer: ^0.35.0 path: ^1.1.0 pool: ^1.3.0 From 382698aacd63a8527aed59fb78b6195c38fedce4 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Mon, 29 Apr 2019 15:46:24 -0700 Subject: [PATCH 49/88] Allow latest pkg:analyzer in example, test on oldest supported SDK (#43) Enable one more lint, remove one deprecated lint Bump min SDK --- pkgs/graphs/.travis.yml | 7 +++---- pkgs/graphs/CHANGELOG.md | 6 +++++- pkgs/graphs/analysis_options.yaml | 2 +- pkgs/graphs/pubspec.yaml | 6 +++--- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pkgs/graphs/.travis.yml b/pkgs/graphs/.travis.yml index 3c0743f6ed..44cbea1060 100644 --- a/pkgs/graphs/.travis.yml +++ b/pkgs/graphs/.travis.yml @@ -2,17 +2,16 @@ language: dart dart: - dev - - stable + - 2.2.0 dart_task: - test - test -p chrome,firefox - - dartfmt - dartanalyzer: --fatal-infos --fatal-warnings . matrix: - exclude: - - dart: stable + include: + - dart: dev dart_task: dartfmt branches: diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index 78722c468b..4dbcf1914f 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,4 +1,8 @@ -# 0.2.0-dev +# 0.2.1 + +- Require Dart SDK `>=2.2.0 <3.0.0`. + +# 0.2.0 - **BREAKING** `shortestPath`, `shortestPaths` and `stronglyConnectedComponents` now have one generic parameter and have replaced the `key` parameter with diff --git a/pkgs/graphs/analysis_options.yaml b/pkgs/graphs/analysis_options.yaml index d6bd4308fc..5e2360946b 100644 --- a/pkgs/graphs/analysis_options.yaml +++ b/pkgs/graphs/analysis_options.yaml @@ -46,12 +46,12 @@ linter: - prefer_conditional_assignment - prefer_const_constructors - prefer_final_fields + - prefer_generic_function_type_aliases - prefer_initializing_formals - prefer_interpolation_to_compose_strings - prefer_single_quotes - prefer_typing_uninitialized_variables - slash_for_doc_comments - - super_goes_last - test_types_in_equals - throw_in_finally - type_init_formals diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index c4c4871ee4..bf6f9d3542 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,17 +1,17 @@ name: graphs -version: 0.2.0 +version: 0.2.1-dev description: Graph algorithms that operate on graphs in any representation author: Dart Team homepage: https://github.com/dart-lang/graphs environment: - sdk: '>=2.0.0 <3.0.0' + sdk: '>=2.2.0 <3.0.0' dev_dependencies: pedantic: ^1.3.0 test: ^1.5.1 # For examples - analyzer: ^0.35.0 + analyzer: '>=0.35.0 <0.37.0' path: ^1.1.0 pool: ^1.3.0 From ccf20c956f5a95f4bd80bbd0e7c10d9b0fa92ca5 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Fri, 19 Jul 2019 21:18:39 -0700 Subject: [PATCH 50/88] update analyzer dep (#44) --- pkgs/graphs/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index bf6f9d3542..1dd6a0836b 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -12,6 +12,6 @@ dev_dependencies: test: ^1.5.1 # For examples - analyzer: '>=0.35.0 <0.37.0' + analyzer: '>=0.35.0 <0.38.0' path: ^1.1.0 pool: ^1.3.0 From d13d0657418de264ca32c0c452e4f7855f3b855d Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 29 Oct 2019 18:24:10 -0700 Subject: [PATCH 51/88] Support latest pkg:analyzer (used in example) (#45) --- pkgs/graphs/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 1dd6a0836b..1d9202c791 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -12,6 +12,6 @@ dev_dependencies: test: ^1.5.1 # For examples - analyzer: '>=0.35.0 <0.38.0' + analyzer: '>=0.35.0 <0.40.0' path: ^1.1.0 pool: ^1.3.0 From fac38fdbf9358a9aafe5c34e6e2747aab0cff262 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Wed, 6 Nov 2019 13:34:02 -0800 Subject: [PATCH 52/88] Fix strict-inference violations (#46) Add type annotations to all arguments and generics where they can't be inferred and fall back to `dynamic`. --- pkgs/graphs/analysis_options.yaml | 2 ++ pkgs/graphs/lib/src/shortest_path.dart | 2 +- pkgs/graphs/lib/src/strongly_connected_components.dart | 2 +- pkgs/graphs/test/shortest_path_test.dart | 6 +++--- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pkgs/graphs/analysis_options.yaml b/pkgs/graphs/analysis_options.yaml index 5e2360946b..eb0c9d237c 100644 --- a/pkgs/graphs/analysis_options.yaml +++ b/pkgs/graphs/analysis_options.yaml @@ -2,6 +2,8 @@ include: package:pedantic/analysis_options.yaml analyzer: strong-mode: implicit-casts: false + language: + strict-inference: true errors: unused_element: error unused_import: error diff --git a/pkgs/graphs/lib/src/shortest_path.dart b/pkgs/graphs/lib/src/shortest_path.dart index f295e5d850..81a5e7e2ca 100644 --- a/pkgs/graphs/lib/src/shortest_path.dart +++ b/pkgs/graphs/lib/src/shortest_path.dart @@ -134,4 +134,4 @@ Map> _shortestPaths( return distances; } -bool _defaultEquals(a, b) => a == b; +bool _defaultEquals(Object a, Object b) => a == b; diff --git a/pkgs/graphs/lib/src/strongly_connected_components.dart b/pkgs/graphs/lib/src/strongly_connected_components.dart index 19519a8b61..1e7a283061 100644 --- a/pkgs/graphs/lib/src/strongly_connected_components.dart +++ b/pkgs/graphs/lib/src/strongly_connected_components.dart @@ -78,4 +78,4 @@ List> stronglyConnectedComponents( return result; } -bool _defaultEquals(a, b) => a == b; +bool _defaultEquals(Object a, Object b) => a == b; diff --git a/pkgs/graphs/test/shortest_path_test.dart b/pkgs/graphs/test/shortest_path_test.dart index f64da09861..3e380678c5 100644 --- a/pkgs/graphs/test/shortest_path_test.dart +++ b/pkgs/graphs/test/shortest_path_test.dart @@ -10,7 +10,7 @@ import 'package:test/test.dart'; import 'utils/utils.dart'; -Matcher _throwsAssertionError(messageMatcher) => +Matcher _throwsAssertionError(dynamic messageMatcher) => throwsA(const TypeMatcher() .having((ae) => ae.message, 'message', messageMatcher)); @@ -44,9 +44,9 @@ void main() { }); test('null return value from `edges` throws', () { - expect(shortestPath(1, 1, (input) => null), [], + expect(shortestPath(1, 1, (input) => null), [], reason: 'self target short-circuits'); - expect(shortestPath(1, 1, (input) => [null]), [], + expect(shortestPath(1, 1, (input) => [null]), [], reason: 'self target short-circuits'); expect(() => shortestPath(1, 2, (input) => null), throwsNoSuchMethodError); From 3271470911cda8f81bdf2a3981275f2253beaa7d Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Fri, 6 Dec 2019 15:01:51 -0800 Subject: [PATCH 53/88] Fix newly enforced package:pedantic lints (#48) - use_function_type_syntax_for_parameters Drop unused author field from pubspec. --- pkgs/graphs/lib/src/shortest_path.dart | 12 ++++++------ .../lib/src/strongly_connected_components.dart | 4 ++-- pkgs/graphs/pubspec.yaml | 1 - 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pkgs/graphs/lib/src/shortest_path.dart b/pkgs/graphs/lib/src/shortest_path.dart index 81a5e7e2ca..f0fd598f56 100644 --- a/pkgs/graphs/lib/src/shortest_path.dart +++ b/pkgs/graphs/lib/src/shortest_path.dart @@ -28,8 +28,8 @@ List shortestPath( T start, T target, Iterable Function(T) edges, { - bool equals(T key1, T key2), - int hashCode(T key), + bool Function(T, T) equals, + int Function(T) hashCode, }) => _shortestPaths( start, @@ -61,8 +61,8 @@ List shortestPath( Map> shortestPaths( T start, Iterable Function(T) edges, { - bool equals(T key1, T key2), - int hashCode(T key), + bool Function(T, T) equals, + int Function(T) hashCode, }) => _shortestPaths( start, @@ -75,8 +75,8 @@ Map> _shortestPaths( T start, Iterable Function(T) edges, { T target, - bool equals(T key1, T key2), - int hashCode(T key), + bool Function(T, T) equals, + int Function(T) hashCode, }) { assert(start != null, '`start` cannot be null'); assert(edges != null, '`edges` cannot be null'); diff --git a/pkgs/graphs/lib/src/strongly_connected_components.dart b/pkgs/graphs/lib/src/strongly_connected_components.dart index 1e7a283061..26018b91d8 100644 --- a/pkgs/graphs/lib/src/strongly_connected_components.dart +++ b/pkgs/graphs/lib/src/strongly_connected_components.dart @@ -32,8 +32,8 @@ import 'dart:math' show min; List> stronglyConnectedComponents( Iterable nodes, Iterable Function(T) edges, { - bool equals(T key1, T key2), - int hashCode(T key), + bool Function(T, T) equals, + int Function(T) hashCode, }) { final result = >[]; final lowLinks = HashMap(equals: equals, hashCode: hashCode); diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 1d9202c791..7f475becf3 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,7 +1,6 @@ name: graphs version: 0.2.1-dev description: Graph algorithms that operate on graphs in any representation -author: Dart Team homepage: https://github.com/dart-lang/graphs environment: From ece772729cc6195edc1ace6e91bb5d8610fcc80e Mon Sep 17 00:00:00 2001 From: Michael R Fairhurst Date: Tue, 22 Sep 2020 12:42:55 -0700 Subject: [PATCH 54/88] Remove unused dart:async imports (#49) As of Dart 2.1, Future and Stream are exported from dart:core --- pkgs/graphs/test/utils/graph.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/pkgs/graphs/test/utils/graph.dart b/pkgs/graphs/test/utils/graph.dart index 29988abcec..f14efe0f6d 100644 --- a/pkgs/graphs/test/utils/graph.dart +++ b/pkgs/graphs/test/utils/graph.dart @@ -2,7 +2,6 @@ // 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. -import 'dart:async'; import 'dart:collection'; import 'utils.dart'; From 81de4a8dd1908b28cf0c353192f0c371088de9fc Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Wed, 23 Sep 2020 12:23:40 -0700 Subject: [PATCH 55/88] bump pkg:analyzer dependency (#50) --- pkgs/graphs/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 7f475becf3..cf0075bea8 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -11,6 +11,6 @@ dev_dependencies: test: ^1.5.1 # For examples - analyzer: '>=0.35.0 <0.40.0' + analyzer: '>=0.35.0 <0.41.0' path: ^1.1.0 pool: ^1.3.0 From b6ef7d976cf43ea25fc2038a93555e6e3e1224c5 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Wed, 28 Oct 2020 16:22:48 -0700 Subject: [PATCH 56/88] Remove unused dart:async imports. (#51) As of Dart 2.1, Future/Stream have been exported from dart:core. --- pkgs/graphs/test/crawl_async_test.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkgs/graphs/test/crawl_async_test.dart b/pkgs/graphs/test/crawl_async_test.dart index 892a0bd5d8..4a0011beb5 100644 --- a/pkgs/graphs/test/crawl_async_test.dart +++ b/pkgs/graphs/test/crawl_async_test.dart @@ -2,8 +2,6 @@ // 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. -import 'dart:async'; - import 'package:test/test.dart'; import 'package:graphs/graphs.dart'; From 1d23ba3289396165c5a95954c5eb227dce809c47 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Thu, 21 Jan 2021 00:31:15 -0800 Subject: [PATCH 57/88] Migrate to GitHub Actions (#53) * Fix List deprecation * Support old sdks --- pkgs/graphs/.github/workflows/ci.yml | 64 ++++++++++++++++++++++++++ pkgs/graphs/.travis.yml | 22 --------- pkgs/graphs/lib/src/shortest_path.dart | 9 ++-- pkgs/graphs/pubspec.yaml | 4 +- 4 files changed, 71 insertions(+), 28 deletions(-) create mode 100644 pkgs/graphs/.github/workflows/ci.yml delete mode 100644 pkgs/graphs/.travis.yml diff --git a/pkgs/graphs/.github/workflows/ci.yml b/pkgs/graphs/.github/workflows/ci.yml new file mode 100644 index 0000000000..687adbd791 --- /dev/null +++ b/pkgs/graphs/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: CI + +on: + # Run on PRs and pushes to the default branch. + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + - cron: "0 0 * * 0" + +env: + PUB_ENVIRONMENT: bot.github + +jobs: + # Check code formatting and static analysis on a single OS (linux) + # against Dart dev. + analyze: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sdk: [dev] + steps: + - uses: actions/checkout@v2 + - uses: dart-lang/setup-dart@v0.3 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Check formatting + run: dart format --output=none --set-exit-if-changed . + if: always() && steps.install.outcome == 'success' + - name: Analyze code + run: dart analyze --fatal-infos + if: always() && steps.install.outcome == 'success' + + # Run tests on a matrix consisting of two dimensions: + # 1. OS: ubuntu-latest, (macos-latest, windows-latest) + # 2. release channel: dev + test: + needs: analyze + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Add macos-latest and/or windows-latest if relevant for this package. + os: [ubuntu-latest] + sdk: [2.3.0, dev] + steps: + - uses: actions/checkout@v2 + - uses: dart-lang/setup-dart@v0.3 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: pub get + - name: Run VM tests + run: pub run test --platform vm + if: always() && steps.install.outcome == 'success' + - name: Run Chrome tests + run: pub run test --platform chrome + if: always() && steps.install.outcome == 'success' diff --git a/pkgs/graphs/.travis.yml b/pkgs/graphs/.travis.yml deleted file mode 100644 index 44cbea1060..0000000000 --- a/pkgs/graphs/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: dart - -dart: - - dev - - 2.2.0 - -dart_task: - - test - - test -p chrome,firefox - - dartanalyzer: --fatal-infos --fatal-warnings . - -matrix: - include: - - dart: dev - dart_task: dartfmt - -branches: - only: [master] - -cache: - directories: - - $HOME/.pub-cache diff --git a/pkgs/graphs/lib/src/shortest_path.dart b/pkgs/graphs/lib/src/shortest_path.dart index f0fd598f56..b58afb5e6d 100644 --- a/pkgs/graphs/lib/src/shortest_path.dart +++ b/pkgs/graphs/lib/src/shortest_path.dart @@ -82,7 +82,7 @@ Map> _shortestPaths( assert(edges != null, '`edges` cannot be null'); final distances = HashMap>(equals: equals, hashCode: hashCode); - distances[start] = List(0); + distances[start] = const []; equals ??= _defaultEquals; if (equals(start, target)) { @@ -112,9 +112,10 @@ Map> _shortestPaths( existingPath.length <= (currentPathLength + 1)); if (existingPath == null) { - final newOption = List(currentPathLength + 1) - ..setRange(0, currentPathLength, currentPath) - ..[currentPathLength] = edge; + final newOption = [ + ...currentPath, + edge, + ]; if (equals(edge, target)) { assert(bestOption == null || bestOption.length > newOption.length); diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index cf0075bea8..c27d475b05 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -4,13 +4,13 @@ description: Graph algorithms that operate on graphs in any representation homepage: https://github.com/dart-lang/graphs environment: - sdk: '>=2.2.0 <3.0.0' + sdk: '>=2.3.0 <3.0.0' dev_dependencies: pedantic: ^1.3.0 test: ^1.5.1 # For examples - analyzer: '>=0.35.0 <0.41.0' + analyzer: '>=0.35.0 <0.42.0' path: ^1.1.0 pool: ^1.3.0 From 54e64584988a6e6837f5c8febf65415b6b5758f3 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Fri, 19 Feb 2021 12:46:38 -0800 Subject: [PATCH 58/88] Remove diagnostic upgrades to errors (#54) We run the analyzer with --fatal-infos so it is not necessary to upgrade warnings or hints to errors. Remove the lint rules that are duplicative with `pedantic`. --- pkgs/graphs/analysis_options.yaml | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/pkgs/graphs/analysis_options.yaml b/pkgs/graphs/analysis_options.yaml index eb0c9d237c..25de3dcade 100644 --- a/pkgs/graphs/analysis_options.yaml +++ b/pkgs/graphs/analysis_options.yaml @@ -4,18 +4,9 @@ analyzer: implicit-casts: false language: strict-inference: true - errors: - unused_element: error - unused_import: error - unused_local_variable: error - dead_code: error linter: rules: - - annotate_overrides - avoid_function_literals_in_foreach_calls - - avoid_init_to_null - - avoid_null_checks_in_equality_operators - - avoid_relative_lib_imports - avoid_returning_null - avoid_unused_constructor_parameters - await_only_futures @@ -25,43 +16,27 @@ linter: - constant_identifier_names - control_flow_in_finally - directives_ordering - - empty_catches - - empty_constructor_bodies - empty_statements - hash_and_equals - implementation_imports - invariant_booleans - iterable_contains_unrelated_type - - library_names - - library_prefixes - list_remove_unrelated_type - no_adjacent_strings_in_list - non_constant_identifier_names - - omit_local_variable_types - only_throw_errors - overridden_fields - package_api_docs - package_names - package_prefixed_library_names - - prefer_adjacent_string_concatenation - - prefer_collection_literals - - prefer_conditional_assignment - prefer_const_constructors - - prefer_final_fields - - prefer_generic_function_type_aliases - prefer_initializing_formals - prefer_interpolation_to_compose_strings - - prefer_single_quotes - prefer_typing_uninitialized_variables - - slash_for_doc_comments - test_types_in_equals - throw_in_finally - - type_init_formals - unnecessary_brace_in_string_interps - - unnecessary_const - unnecessary_getters_setters - unnecessary_lambdas - - unnecessary_new - unnecessary_null_aware_assignments - unnecessary_statements - - unnecessary_this From 1d3035d37fb0e421ef43e881069e5f82ec0df137 Mon Sep 17 00:00:00 2001 From: Jacob MacDonald Date: Tue, 23 Feb 2021 17:06:28 -0800 Subject: [PATCH 59/88] Simplify shortestPath, and optimize it a bit (#56) * Simplify shortestPath and make the logic more clear. * udpate pubspec/changelog * update sdk constraint * update to 2.8.1 as 2.8.0 doesn't exist? * api docs are wrong it didnt exist until 2.9.0 * add customized iterable to reduce extra list allocations * fix the iterator so that it wont stack overflow * clean up the impl a bit * fix test and commit worst case benchmark * fix benchmark asserts, access first to make them representative * Update copyright date Co-authored-by: Nate Bosch * fix assert message Co-authored-by: Nate Bosch --- pkgs/graphs/.github/workflows/ci.yml | 2 +- pkgs/graphs/CHANGELOG.md | 6 ++ .../benchmark/shortest_path_benchmark.dart | 7 +- .../shortest_path_worst_case_benchmark.dart | 56 ++++++++++++++ pkgs/graphs/lib/src/shortest_path.dart | 77 +++++++++++-------- pkgs/graphs/pubspec.yaml | 4 +- pkgs/graphs/test/shortest_path_test.dart | 4 +- 7 files changed, 118 insertions(+), 38 deletions(-) create mode 100644 pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart diff --git a/pkgs/graphs/.github/workflows/ci.yml b/pkgs/graphs/.github/workflows/ci.yml index 687adbd791..9315aa8584 100644 --- a/pkgs/graphs/.github/workflows/ci.yml +++ b/pkgs/graphs/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: matrix: # Add macos-latest and/or windows-latest if relevant for this package. os: [ubuntu-latest] - sdk: [2.3.0, dev] + sdk: [2.9.0, dev] steps: - uses: actions/checkout@v2 - uses: dart-lang/setup-dart@v0.3 diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index 4dbcf1914f..a373209d8b 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.0.0-dev + +- **Breaking**: Paths from `shortestPath[s]` are now returned as iterables to + reduce memory consumption of the algorithm to O(n). +- Require Dart SDK `>=2.9.0 <3.0.0`. + # 0.2.1 - Require Dart SDK `>=2.2.0 <3.0.0`. diff --git a/pkgs/graphs/benchmark/shortest_path_benchmark.dart b/pkgs/graphs/benchmark/shortest_path_benchmark.dart index 813c18b04f..87d3f21b0b 100644 --- a/pkgs/graphs/benchmark/shortest_path_benchmark.dart +++ b/pkgs/graphs/benchmark/shortest_path_benchmark.dart @@ -27,16 +27,19 @@ void main() { final testOutput = shortestPath(0, size - 1, (e) => graph[e] ?? []).toString(); print(testOutput); - assert(testOutput == '[258, 252, 819, 999]'); + assert(testOutput == '(258, 252, 819, 999)', testOutput); final watch = Stopwatch(); for (var i = 1;; i++) { watch ..reset() ..start(); - final length = shortestPath(0, size - 1, (e) => graph[e] ?? []).length; + final result = shortestPath(0, size - 1, (e) => graph[e] ?? []); + final length = result.length; + final first = result.first; watch.stop(); assert(length == 4, '$length'); + assert(first == 258, '$first'); if (minTicks == null || watch.elapsedTicks < minTicks) { minTicks = watch.elapsedTicks; diff --git a/pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart b/pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart new file mode 100644 index 0000000000..dbfb91b588 --- /dev/null +++ b/pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart @@ -0,0 +1,56 @@ +// Copyright (c) 2020, 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. + +import 'dart:collection'; + +import 'package:graphs/graphs.dart'; + +void main() { + final size = 1000; + final graph = HashMap>(); + + // We create a graph where every subsequent node has an edge to every other + // node before it as well as the next node. This triggers worst case behavior + // in many algorithms as it requires visiting all nodes and edges before + // finding a solution, and there are a maximum number of edges. + for (var i = 0; i < size; i++) { + final toList = graph.putIfAbsent(i, () => []); + for (var t = 0; t < i + 2 && i < size; t++) { + if (i == t) continue; + toList.add(t); + } + } + + int minTicks; + var maxIteration = 0; + + final testOutput = + shortestPath(0, size - 1, (e) => graph[e] ?? []).toString(); + print(testOutput); + assert(testOutput == Iterable.generate(size - 1, (i) => i + 1).toString(), + '$testOutput'); + + final watch = Stopwatch(); + for (var i = 1;; i++) { + watch + ..reset() + ..start(); + final result = shortestPath(0, size - 1, (e) => graph[e] ?? []); + final length = result.length; + final first = result.first; + watch.stop(); + assert(length == 999, '$length'); + assert(first == 1, '$first'); + + if (minTicks == null || watch.elapsedTicks < minTicks) { + minTicks = watch.elapsedTicks; + maxIteration = i; + } + + if (maxIteration == i || (i - maxIteration) % 100000 == 0) { + print('min ticks for one run: $minTicks\t' + 'after $maxIteration of $i iterations'); + } + } +} diff --git a/pkgs/graphs/lib/src/shortest_path.dart b/pkgs/graphs/lib/src/shortest_path.dart index b58afb5e6d..c8b8d9277d 100644 --- a/pkgs/graphs/lib/src/shortest_path.dart +++ b/pkgs/graphs/lib/src/shortest_path.dart @@ -24,7 +24,7 @@ import 'dart:collection'; /// /// If you supply one of [equals] or [hashCode], you should generally also to /// supply the other. -List shortestPath( +Iterable shortestPath( T start, T target, Iterable Function(T) edges, { @@ -58,7 +58,7 @@ List shortestPath( /// /// If you supply one of [equals] or [hashCode], you should generally also to /// supply the other. -Map> shortestPaths( +Map> shortestPaths( T start, Iterable Function(T) edges, { bool Function(T, T) equals, @@ -71,7 +71,7 @@ Map> shortestPaths( hashCode: hashCode, ); -Map> _shortestPaths( +Map> _shortestPaths( T start, Iterable Function(T) edges, { T target, @@ -81,8 +81,8 @@ Map> _shortestPaths( assert(start != null, '`start` cannot be null'); assert(edges != null, '`edges` cannot be null'); - final distances = HashMap>(equals: equals, hashCode: hashCode); - distances[start] = const []; + final distances = HashMap>(equals: equals, hashCode: hashCode); + distances[start] = _Tail(); equals ??= _defaultEquals; if (equals(start, target)) { @@ -91,43 +91,20 @@ Map> _shortestPaths( final toVisit = ListQueue()..add(start); - List bestOption; - while (toVisit.isNotEmpty) { final current = toVisit.removeFirst(); final currentPath = distances[current]; - final currentPathLength = currentPath.length; - - if (bestOption != null && (currentPathLength + 1) >= bestOption.length) { - // Skip any existing `toVisit` items that have no chance of being - // better than bestOption (if it exists) - continue; - } for (var edge in edges(current)) { assert(edge != null, '`edges` cannot return null values.'); final existingPath = distances[edge]; - assert(existingPath == null || - existingPath.length <= (currentPathLength + 1)); - if (existingPath == null) { - final newOption = [ - ...currentPath, - edge, - ]; - + distances[edge] = currentPath.append(edge); if (equals(edge, target)) { - assert(bestOption == null || bestOption.length > newOption.length); - bestOption = newOption; - } - - distances[edge] = newOption; - if (bestOption == null || bestOption.length > newOption.length) { - // Only add a node to visit if it might be a better path to the - // target node - toVisit.add(edge); + return distances; } + toVisit.add(edge); } } } @@ -136,3 +113,41 @@ Map> _shortestPaths( } bool _defaultEquals(Object a, Object b) => a == b; + +/// An immutable iterable that can efficiently return a copy with a value +/// appended. +/// +/// This implementation has an efficient [length] property. +/// +/// Note that grabbing an [iterator] for the first time is O(n) in time and +/// space because it copies all the values to a new list and uses that +/// iterator in order to avoid stack overflows for large paths. This copy is +/// cached for subsequent calls. +class _Tail extends Iterable { + final T /*?*/ tail; + final _Tail /*?*/ head; + @override + final int length; + _Tail() + : tail = null, + head = null, + length = 0; + _Tail._(this.tail, this.head, this.length); + _Tail append(T value) => _Tail._(value, this, length + 1); + + Iterator /*?*/ _iterator; + + @override + Iterator get iterator { + if (_iterator == null) { + var /*_Tail?*/ next = this; + var values = List.generate(length, (_) { + var val = next.tail; + next = next.head; + return val; + }); + _iterator = values.reversed.iterator; + } + return _iterator; + } +} diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index c27d475b05..9e4dee8ba2 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,10 +1,10 @@ name: graphs -version: 0.2.1-dev +version: 1.0.0-dev description: Graph algorithms that operate on graphs in any representation homepage: https://github.com/dart-lang/graphs environment: - sdk: '>=2.3.0 <3.0.0' + sdk: '>=2.9.0 <3.0.0' dev_dependencies: pedantic: ^1.3.0 diff --git a/pkgs/graphs/test/shortest_path_test.dart b/pkgs/graphs/test/shortest_path_test.dart index 3e380678c5..7efc5dd4f3 100644 --- a/pkgs/graphs/test/shortest_path_test.dart +++ b/pkgs/graphs/test/shortest_path_test.dart @@ -127,7 +127,7 @@ void main() { final size = 1000; final graph = HashMap>(); - List resultForGraph() => + Iterable resultForGraph() => shortestPath(0, size - 1, (e) => graph[e] ?? const []); void addRandomEdge() { @@ -139,7 +139,7 @@ void main() { } } - List result; + Iterable result; // Add edges until there is a shortest path between `0` and `size - 1` do { From af25bfe1107194dd85e9e9297b4fb9d9d8da4633 Mon Sep 17 00:00:00 2001 From: hovadur Date: Fri, 26 Feb 2021 02:29:32 +0300 Subject: [PATCH 60/88] Migrate to null safety (#52) --- pkgs/graphs/.github/workflows/ci.yml | 8 +-- pkgs/graphs/CHANGELOG.md | 7 +- .../connected_components_benchmark.dart | 3 +- .../benchmark/shortest_path_benchmark.dart | 6 +- .../shortest_path_worst_case_benchmark.dart | 6 +- pkgs/graphs/example/crawl_async_example.dart | 12 ++-- pkgs/graphs/example/example.dart | 2 +- pkgs/graphs/lib/src/crawl_async.dart | 6 +- pkgs/graphs/lib/src/shortest_path.dart | 71 ++++++++----------- .../src/strongly_connected_components.dart | 19 +++-- pkgs/graphs/pubspec.yaml | 12 ++-- pkgs/graphs/test/crawl_async_test.dart | 11 ++- pkgs/graphs/test/shortest_path_test.dart | 60 ++++------------ .../strongly_connected_components_test.dart | 8 +-- pkgs/graphs/test/utils/graph.dart | 19 ++--- pkgs/graphs/test/utils/utils.dart | 2 +- 16 files changed, 104 insertions(+), 148 deletions(-) diff --git a/pkgs/graphs/.github/workflows/ci.yml b/pkgs/graphs/.github/workflows/ci.yml index 9315aa8584..64e2f8b91f 100644 --- a/pkgs/graphs/.github/workflows/ci.yml +++ b/pkgs/graphs/.github/workflows/ci.yml @@ -14,13 +14,13 @@ env: jobs: # Check code formatting and static analysis on a single OS (linux) - # against Dart dev. + # against Dart beta. analyze: runs-on: ubuntu-latest strategy: fail-fast: false matrix: - sdk: [dev] + sdk: [beta] steps: - uses: actions/checkout@v2 - uses: dart-lang/setup-dart@v0.3 @@ -38,7 +38,7 @@ jobs: # Run tests on a matrix consisting of two dimensions: # 1. OS: ubuntu-latest, (macos-latest, windows-latest) - # 2. release channel: dev + # 2. release channel: beta test: needs: analyze runs-on: ${{ matrix.os }} @@ -47,7 +47,7 @@ jobs: matrix: # Add macos-latest and/or windows-latest if relevant for this package. os: [ubuntu-latest] - sdk: [2.9.0, dev] + sdk: [beta] steps: - uses: actions/checkout@v2 - uses: dart-lang/setup-dart@v0.3 diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index a373209d8b..f5a4fe7d02 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,12 +1,9 @@ # 1.0.0-dev + +- Migrate to null safety. - **Breaking**: Paths from `shortestPath[s]` are now returned as iterables to reduce memory consumption of the algorithm to O(n). -- Require Dart SDK `>=2.9.0 <3.0.0`. - -# 0.2.1 - -- Require Dart SDK `>=2.2.0 <3.0.0`. # 0.2.0 diff --git a/pkgs/graphs/benchmark/connected_components_benchmark.dart b/pkgs/graphs/benchmark/connected_components_benchmark.dart index c1c83d7820..16a15d0d0d 100644 --- a/pkgs/graphs/benchmark/connected_components_benchmark.dart +++ b/pkgs/graphs/benchmark/connected_components_benchmark.dart @@ -32,7 +32,8 @@ void main() { while (watch.elapsed < duration) { count++; final length = - stronglyConnectedComponents(graph.keys, (e) => graph[e] ?? []).length; + stronglyConnectedComponents(graph.keys, (e) => graph[e] ?? []) + .length; assert(length == 244, '$length'); } diff --git a/pkgs/graphs/benchmark/shortest_path_benchmark.dart b/pkgs/graphs/benchmark/shortest_path_benchmark.dart index 87d3f21b0b..c06dc2c4c0 100644 --- a/pkgs/graphs/benchmark/shortest_path_benchmark.dart +++ b/pkgs/graphs/benchmark/shortest_path_benchmark.dart @@ -21,11 +21,11 @@ void main() { } } - int minTicks; + int? minTicks; var maxIteration = 0; final testOutput = - shortestPath(0, size - 1, (e) => graph[e] ?? []).toString(); + shortestPath(0, size - 1, (e) => graph[e] ?? []).toString(); print(testOutput); assert(testOutput == '(258, 252, 819, 999)', testOutput); @@ -34,7 +34,7 @@ void main() { watch ..reset() ..start(); - final result = shortestPath(0, size - 1, (e) => graph[e] ?? []); + final result = shortestPath(0, size - 1, (e) => graph[e] ?? [])!; final length = result.length; final first = result.first; watch.stop(); diff --git a/pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart b/pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart index dbfb91b588..cbacf62352 100644 --- a/pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart +++ b/pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart @@ -22,11 +22,11 @@ void main() { } } - int minTicks; + int? minTicks; var maxIteration = 0; final testOutput = - shortestPath(0, size - 1, (e) => graph[e] ?? []).toString(); + shortestPath(0, size - 1, (e) => graph[e] ?? []).toString(); print(testOutput); assert(testOutput == Iterable.generate(size - 1, (i) => i + 1).toString(), '$testOutput'); @@ -36,7 +36,7 @@ void main() { watch ..reset() ..start(); - final result = shortestPath(0, size - 1, (e) => graph[e] ?? []); + final result = shortestPath(0, size - 1, (e) => graph[e] ?? [])!; final length = result.length; final first = result.first; watch.stop(); diff --git a/pkgs/graphs/example/crawl_async_example.dart b/pkgs/graphs/example/crawl_async_example.dart index ce4c3e39d8..d6f3e40f97 100644 --- a/pkgs/graphs/example/crawl_async_example.dart +++ b/pkgs/graphs/example/crawl_async_example.dart @@ -26,10 +26,11 @@ Future main() async { print(allImports.map((s) => s.uri).toList()); } -AnalysisContext _analysisContext; +AnalysisContext? _analysisContext; Future get analysisContext async { - if (_analysisContext == null) { + var context = _analysisContext; + if (context == null) { var libUri = Uri.parse('package:graphs/'); var libPath = await pathForUri(libUri); var packagePath = p.dirname(libPath); @@ -39,16 +40,17 @@ Future get analysisContext async { throw StateError('Expected to find exactly one context root, got $roots'); } - _analysisContext = ContextBuilder().createContext(contextRoot: roots[0]); + context = _analysisContext = + ContextBuilder().createContext(contextRoot: roots[0]); } - return _analysisContext; + return context; } Future> findImports(Uri from, Source source) async { return source.unit.directives .whereType() - .map((d) => d.uri.stringValue) + .map((d) => d.uri.stringValue!) .where((uri) => !uri.startsWith('dart:')) .map((import) => resolveImport(import, from)); } diff --git a/pkgs/graphs/example/example.dart b/pkgs/graphs/example/example.dart index 47aca6177d..2fbb8e16f2 100644 --- a/pkgs/graphs/example/example.dart +++ b/pkgs/graphs/example/example.dart @@ -41,7 +41,7 @@ void main() { }); var components = stronglyConnectedComponents( - graph.nodes.keys, (node) => graph.nodes[node]); + graph.nodes.keys, (node) => graph.nodes[node] ?? []); print(components); } diff --git a/pkgs/graphs/lib/src/crawl_async.dart b/pkgs/graphs/lib/src/crawl_async.dart index 2cf0f54189..264ce1d28a 100644 --- a/pkgs/graphs/lib/src/crawl_async.dart +++ b/pkgs/graphs/lib/src/crawl_async.dart @@ -33,7 +33,9 @@ final _empty = Future.value(null); /// Crawling is eager, so calls to [edges] may overlap with other calls that /// have not completed. If the [edges] callback needs to be limited or throttled /// that must be done by wrapping it before calling [crawlAsync]. -Stream crawlAsync(Iterable roots, FutureOr Function(K) readNode, +Stream crawlAsync( + Iterable roots, + FutureOr Function(K) readNode, FutureOr> Function(K, V) edges) { final crawl = _CrawlAsync(roots, readNode, edges)..run(); return crawl.result.stream; @@ -69,7 +71,7 @@ class _CrawlAsync { if (value == null) return; if (result.isClosed) return; result.add(value); - var next = await edges(key, value) ?? const []; + var next = await edges(key, value); await Future.wait(next.map(_visit), eagerError: true); } diff --git a/pkgs/graphs/lib/src/shortest_path.dart b/pkgs/graphs/lib/src/shortest_path.dart index c8b8d9277d..bf23620808 100644 --- a/pkgs/graphs/lib/src/shortest_path.dart +++ b/pkgs/graphs/lib/src/shortest_path.dart @@ -10,11 +10,6 @@ import 'dart:collection'; /// If [start] `==` [target], an empty [List] is returned and [edges] is never /// called. /// -/// [start], [target] and all values returned by [edges] must not be `null`. -/// If asserts are enabled, an [AssertionError] is raised if these conditions -/// are not met. If asserts are not enabled, violations result in undefined -/// behavior. -/// /// If [equals] is provided, it is used to compare nodes in the graph. If /// [equals] is omitted, the node's own [Object.==] is used instead. /// @@ -24,12 +19,12 @@ import 'dart:collection'; /// /// If you supply one of [equals] or [hashCode], you should generally also to /// supply the other. -Iterable shortestPath( +Iterable? shortestPath( T start, T target, Iterable Function(T) edges, { - bool Function(T, T) equals, - int Function(T) hashCode, + bool Function(T, T)? equals, + int Function(T)? hashCode, }) => _shortestPaths( start, @@ -58,11 +53,11 @@ Iterable shortestPath( /// /// If you supply one of [equals] or [hashCode], you should generally also to /// supply the other. -Map> shortestPaths( +Map> shortestPaths( T start, Iterable Function(T) edges, { - bool Function(T, T) equals, - int Function(T) hashCode, + bool Function(T, T)? equals, + int Function(T)? hashCode, }) => _shortestPaths( start, @@ -71,21 +66,20 @@ Map> shortestPaths( hashCode: hashCode, ); -Map> _shortestPaths( +Map> _shortestPaths( T start, Iterable Function(T) edges, { - T target, - bool Function(T, T) equals, - int Function(T) hashCode, + T? target, + bool Function(T, T)? equals, + int Function(T)? hashCode, }) { - assert(start != null, '`start` cannot be null'); - assert(edges != null, '`edges` cannot be null'); - final distances = HashMap>(equals: equals, hashCode: hashCode); distances[start] = _Tail(); - equals ??= _defaultEquals; - if (equals(start, target)) { + final nonNullEquals = equals ??= _defaultEquals; + final isTarget = + target == null ? _neverTarget : (T node) => nonNullEquals(node, target); + if (isTarget(start)) { return distances; } @@ -93,15 +87,14 @@ Map> _shortestPaths( while (toVisit.isNotEmpty) { final current = toVisit.removeFirst(); - final currentPath = distances[current]; + final currentPath = distances[current]!; for (var edge in edges(current)) { - assert(edge != null, '`edges` cannot return null values.'); final existingPath = distances[edge]; if (existingPath == null) { distances[edge] = currentPath.append(edge); - if (equals(edge, target)) { + if (isTarget(edge)) { return distances; } toVisit.add(edge); @@ -113,6 +106,7 @@ Map> _shortestPaths( } bool _defaultEquals(Object a, Object b) => a == b; +bool _neverTarget(Object _) => false; /// An immutable iterable that can efficiently return a copy with a value /// appended. @@ -123,9 +117,9 @@ bool _defaultEquals(Object a, Object b) => a == b; /// space because it copies all the values to a new list and uses that /// iterator in order to avoid stack overflows for large paths. This copy is /// cached for subsequent calls. -class _Tail extends Iterable { - final T /*?*/ tail; - final _Tail /*?*/ head; +class _Tail extends Iterable { + final T? tail; + final _Tail? head; @override final int length; _Tail() @@ -135,19 +129,16 @@ class _Tail extends Iterable { _Tail._(this.tail, this.head, this.length); _Tail append(T value) => _Tail._(value, this, length + 1); - Iterator /*?*/ _iterator; - @override - Iterator get iterator { - if (_iterator == null) { - var /*_Tail?*/ next = this; - var values = List.generate(length, (_) { - var val = next.tail; - next = next.head; - return val; - }); - _iterator = values.reversed.iterator; - } - return _iterator; - } + Iterator get iterator => _asIterable.iterator; + + late final _asIterable = () { + _Tail? next = this; + var reversed = List.generate(length, (_) { + var val = next!.tail; + next = next!.head; + return val as T; + }); + return reversed.reversed; + }(); } diff --git a/pkgs/graphs/lib/src/strongly_connected_components.dart b/pkgs/graphs/lib/src/strongly_connected_components.dart index 26018b91d8..7021ed38f3 100644 --- a/pkgs/graphs/lib/src/strongly_connected_components.dart +++ b/pkgs/graphs/lib/src/strongly_connected_components.dart @@ -29,35 +29,34 @@ import 'dart:math' show min; /// /// If you supply one of [equals] or [hashCode], you should generally also to /// supply the other. -List> stronglyConnectedComponents( +List> stronglyConnectedComponents( Iterable nodes, Iterable Function(T) edges, { - bool Function(T, T) equals, - int Function(T) hashCode, + bool Function(T, T)? equals, + int Function(T)? hashCode, }) { final result = >[]; final lowLinks = HashMap(equals: equals, hashCode: hashCode); final indexes = HashMap(equals: equals, hashCode: hashCode); final onStack = HashSet(equals: equals, hashCode: hashCode); - equals ??= _defaultEquals; + final nonNullEquals = equals ?? _defaultEquals; var index = 0; var lastVisited = Queue(); void strongConnect(T node) { indexes[node] = index; - lowLinks[node] = index; + var lowLink = lowLinks[node] = index; index++; lastVisited.addLast(node); onStack.add(node); - // ignore: omit_local_variable_types - for (final T next in edges(node) ?? const []) { + for (final next in edges(node)) { if (!indexes.containsKey(next)) { strongConnect(next); - lowLinks[node] = min(lowLinks[node], lowLinks[next]); + lowLink = lowLinks[node] = min(lowLink, lowLinks[next]!); } else if (onStack.contains(next)) { - lowLinks[node] = min(lowLinks[node], indexes[next]); + lowLink = lowLinks[node] = min(lowLink, indexes[next]!); } } if (lowLinks[node] == indexes[node]) { @@ -67,7 +66,7 @@ List> stronglyConnectedComponents( next = lastVisited.removeLast(); onStack.remove(next); component.add(next); - } while (!equals(next, node)); + } while (!nonNullEquals(next, node)); result.add(component); } } diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 9e4dee8ba2..aa12ade38d 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -4,13 +4,13 @@ description: Graph algorithms that operate on graphs in any representation homepage: https://github.com/dart-lang/graphs environment: - sdk: '>=2.9.0 <3.0.0' + sdk: '>=2.12.0-0 <3.0.0' dev_dependencies: - pedantic: ^1.3.0 - test: ^1.5.1 + pedantic: ^1.10.0 + test: ^1.16.0 # For examples - analyzer: '>=0.35.0 <0.42.0' - path: ^1.1.0 - pool: ^1.3.0 + path: ^1.8.0 + pool: ^1.5.0 + analyzer: ^1.0.0 diff --git a/pkgs/graphs/test/crawl_async_test.dart b/pkgs/graphs/test/crawl_async_test.dart index 4a0011beb5..3cb3d4a8f0 100644 --- a/pkgs/graphs/test/crawl_async_test.dart +++ b/pkgs/graphs/test/crawl_async_test.dart @@ -2,16 +2,15 @@ // 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. -import 'package:test/test.dart'; - import 'package:graphs/graphs.dart'; +import 'package:test/test.dart'; import 'utils/graph.dart'; void main() { group('asyncCrawl', () { - Future> crawl( - Map> g, Iterable roots) { + Future> crawl( + Map?> g, Iterable roots) { var graph = AsyncGraph(g); return crawlAsync(roots, graph.readNode, graph.edges).toList(); } @@ -88,7 +87,7 @@ void main() { 'a': ['b'], }; var nodes = crawlAsync(['a'], (n) => n, - (k, n) => k == 'b' ? throw ArgumentError() : graph[k]); + (k, n) => k == 'b' ? throw ArgumentError() : graph[k] ?? []); expect(nodes, emitsThrough(emitsError(isArgumentError))); }); @@ -97,7 +96,7 @@ void main() { 'a': ['b'], }; var nodes = crawlAsync(['a'], (n) => n == 'b' ? throw ArgumentError() : n, - (k, n) => graph[k]); + (k, n) => graph[k] ?? []); expect(nodes, emitsThrough(emitsError(isArgumentError))); }); }); diff --git a/pkgs/graphs/test/shortest_path_test.dart b/pkgs/graphs/test/shortest_path_test.dart index 7efc5dd4f3..89856ab414 100644 --- a/pkgs/graphs/test/shortest_path_test.dart +++ b/pkgs/graphs/test/shortest_path_test.dart @@ -10,10 +10,6 @@ import 'package:test/test.dart'; import 'utils/utils.dart'; -Matcher _throwsAssertionError(dynamic messageMatcher) => - throwsA(const TypeMatcher() - .having((ae) => ae.message, 'message', messageMatcher)); - void main() { const graph = >{ '1': ['2', '5'], @@ -24,42 +20,12 @@ void main() { '6': ['7'], }; - List getValues(String key) => graph[key] ?? []; + List readEdges(String key) => graph[key] ?? []; List getXValues(X key) => - graph[key.value]?.map((v) => X(v))?.toList() ?? []; - - test('null `start` throws AssertionError', () { - expect(() => shortestPath(null, '1', getValues), - _throwsAssertionError('`start` cannot be null')); - expect(() => shortestPaths(null, getValues), - _throwsAssertionError('`start` cannot be null')); - }); - - test('null `edges` throws AssertionError', () { - expect(() => shortestPath(1, 1, null), - _throwsAssertionError('`edges` cannot be null')); - expect(() => shortestPaths(1, null), - _throwsAssertionError('`edges` cannot be null')); - }); - - test('null return value from `edges` throws', () { - expect(shortestPath(1, 1, (input) => null), [], - reason: 'self target short-circuits'); - expect(shortestPath(1, 1, (input) => [null]), [], - reason: 'self target short-circuits'); + graph[key.value]?.map((v) => X(v)).toList() ?? []; - expect(() => shortestPath(1, 2, (input) => null), throwsNoSuchMethodError); - - expect(() => shortestPaths(1, (input) => null), throwsNoSuchMethodError); - - expect(() => shortestPath(1, 2, (input) => [null]), - _throwsAssertionError('`edges` cannot return null values.')); - expect(() => shortestPaths(1, (input) => [null]), - _throwsAssertionError('`edges` cannot return null values.')); - }); - - void _singlePathTest(String from, String to, List expected) { + void _singlePathTest(String from, String to, List? expected) { test('$from -> $to should be $expected (mapped)', () { expect( shortestPath(X(from), X(to), getXValues, @@ -69,7 +35,7 @@ void main() { }); test('$from -> $to should be $expected', () { - expect(shortestPath(from, to, getValues), expected); + expect(shortestPath(from, to, readEdges), expected); }); } @@ -83,7 +49,7 @@ void main() { }); test('paths from $from', () { - final result = shortestPaths(from, getValues); + final result = shortestPaths(from, readEdges); expect(result, expected); }); @@ -118,8 +84,6 @@ void main() { _pathsTest('42', {'42': []}, ['1', '6']); - _singlePathTest('1', null, null); - test('integration test', () { // Be deterministic in the generated graph. This test may have to be updated // if the behavior of `Random` changes for the provided seed. @@ -127,8 +91,8 @@ void main() { final size = 1000; final graph = HashMap>(); - Iterable resultForGraph() => - shortestPath(0, size - 1, (e) => graph[e] ?? const []); + Iterable? resultForGraph() => + shortestPath(0, size - 1, (e) => graph[e] ?? const []); void addRandomEdge() { final toList = graph.putIfAbsent(_rnd.nextInt(size), () => []); @@ -139,7 +103,7 @@ void main() { } } - Iterable result; + Iterable? result; // Add edges until there is a shortest path between `0` and `size - 1` do { @@ -156,10 +120,10 @@ void main() { do { expect(++count, lessThan(size * 5), reason: 'This loop should finish.'); addRandomEdge(); - final previousResultLength = result.length; + final previousResultLength = result!.length; result = resultForGraph(); expect(result, hasLength(lessThanOrEqualTo(previousResultLength))); - } while (result.length > 2); + } while (result!.length > 2); expect(result, [275, 999]); @@ -171,13 +135,13 @@ void main() { do { expect(++count, lessThan(size * 5), reason: 'This loop should finish.'); final randomKey = graph.keys.elementAt(_rnd.nextInt(graph.length)); - final list = graph[randomKey]; + final list = graph[randomKey]!; expect(list, isNotEmpty); list.removeAt(_rnd.nextInt(list.length)); if (list.isEmpty) { graph.remove(randomKey); } - final previousResultLength = result.length; + final previousResultLength = result!.length; result = resultForGraph(); if (result != null) { expect(result, hasLength(greaterThanOrEqualTo(previousResultLength))); diff --git a/pkgs/graphs/test/strongly_connected_components_test.dart b/pkgs/graphs/test/strongly_connected_components_test.dart index 6c3968d449..f147f56f8e 100644 --- a/pkgs/graphs/test/strongly_connected_components_test.dart +++ b/pkgs/graphs/test/strongly_connected_components_test.dart @@ -12,8 +12,8 @@ void main() { group('strongly connected components', () { /// Run [stronglyConnectedComponents] on [g]. List> components( - Map> g, { - Iterable startNodes, + Map?> g, { + Iterable? startNodes, }) { final graph = Graph(g); return stronglyConnectedComponents( @@ -148,8 +148,8 @@ void main() { group('custom hashCode and equals', () { /// Run [stronglyConnectedComponents] on [g]. List> components( - Map> g, { - Iterable startNodes, + Map?> g, { + Iterable? startNodes, }) { final graph = BadGraph(g); diff --git a/pkgs/graphs/test/utils/graph.dart b/pkgs/graphs/test/utils/graph.dart index f14efe0f6d..92c220e58c 100644 --- a/pkgs/graphs/test/utils/graph.dart +++ b/pkgs/graphs/test/utils/graph.dart @@ -8,24 +8,24 @@ import 'utils.dart'; /// A representation of a Graph since none is specified in `lib/`. class Graph { - final Map> _graph; + final Map?> _graph; Graph(this._graph); - List edges(String node) => _graph[node]; + List edges(String node) => _graph[node] ?? []; Iterable get allNodes => _graph.keys; } class BadGraph { - final Map> _graph; + final Map?> _graph; - BadGraph(Map> values) + BadGraph(Map?> values) : _graph = LinkedHashMap(equals: xEquals, hashCode: xHashCode) ..addEntries(values.entries.map( - (e) => MapEntry(X(e.key), e?.value?.map((v) => X(v))?.toList()))); + (e) => MapEntry(X(e.key), e.value?.map((v) => X(v)).toList()))); - List edges(X node) => _graph[node]; + List edges(X node) => _graph[node] ?? []; Iterable get allNodes => _graph.keys; } @@ -33,12 +33,13 @@ class BadGraph { /// A representation of a Graph where keys can asynchronously be resolved to /// real values or to edges. class AsyncGraph { - final Map> graph; + final Map?> graph; AsyncGraph(this.graph); - Future readNode(String node) async => + Future readNode(String node) async => graph.containsKey(node) ? node : null; - Future> edges(String key, String node) async => graph[key]; + Future> edges(String key, String? node) async => + graph[key] ?? []; } diff --git a/pkgs/graphs/test/utils/utils.dart b/pkgs/graphs/test/utils/utils.dart index f4ed52b37d..19853e75e0 100644 --- a/pkgs/graphs/test/utils/utils.dart +++ b/pkgs/graphs/test/utils/utils.dart @@ -2,7 +2,7 @@ // 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. -bool xEquals(X a, X b) => a?.value == b?.value; +bool xEquals(X a, X b) => a.value == b.value; int xHashCode(X a) => a.value.hashCode; From 93007c0094dcd0e1ad16578ce2ef2c766e8c2c08 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Fri, 26 Feb 2021 11:12:06 -0800 Subject: [PATCH 61/88] Prepare to publish (#57) --- pkgs/graphs/CHANGELOG.md | 2 +- pkgs/graphs/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index f5a4fe7d02..b32c6b3f3e 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,4 +1,4 @@ -# 1.0.0-dev +# 1.0.0 - Migrate to null safety. diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index aa12ade38d..5e5dba7e67 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,5 +1,5 @@ name: graphs -version: 1.0.0-dev +version: 1.0.0 description: Graph algorithms that operate on graphs in any representation homepage: https://github.com/dart-lang/graphs From 1809c295e31a8e0ccaa808a7ae247ec0008d8c4a Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Mon, 15 Mar 2021 19:58:53 -0700 Subject: [PATCH 62/88] Stop ignoring null nodes in crawlAsync (#59) Closes #58 Leave the decision about whether `null` is meaningful in another way to the caller. --- pkgs/graphs/.github/workflows/ci.yml | 4 ++-- pkgs/graphs/CHANGELOG.md | 6 +++++- pkgs/graphs/lib/src/crawl_async.dart | 13 ++++--------- pkgs/graphs/pubspec.yaml | 6 +++--- pkgs/graphs/test/crawl_async_test.dart | 4 ++-- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/pkgs/graphs/.github/workflows/ci.yml b/pkgs/graphs/.github/workflows/ci.yml index 64e2f8b91f..f1bba1676e 100644 --- a/pkgs/graphs/.github/workflows/ci.yml +++ b/pkgs/graphs/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - sdk: [beta] + sdk: [dev] steps: - uses: actions/checkout@v2 - uses: dart-lang/setup-dart@v0.3 @@ -47,7 +47,7 @@ jobs: matrix: # Add macos-latest and/or windows-latest if relevant for this package. os: [ubuntu-latest] - sdk: [beta] + sdk: [2.12.0, dev] steps: - uses: actions/checkout@v2 - uses: dart-lang/setup-dart@v0.3 diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index b32c6b3f3e..2515638955 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,5 +1,9 @@ -# 1.0.0 +# 2.0.0-dev + +- **Breaking**: `crawlAsync` will no longer ignore a node from the graph if the + `readNode` callback returns null. +# 1.0.0 - Migrate to null safety. - **Breaking**: Paths from `shortestPath[s]` are now returned as iterables to diff --git a/pkgs/graphs/lib/src/crawl_async.dart b/pkgs/graphs/lib/src/crawl_async.dart index 264ce1d28a..70699ca44a 100644 --- a/pkgs/graphs/lib/src/crawl_async.dart +++ b/pkgs/graphs/lib/src/crawl_async.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'dart:collection'; -final _empty = Future.value(null); +final _empty = Future.value(); /// Finds and returns every node in a graph who's nodes and edges are /// asynchronously resolved. @@ -22,10 +22,6 @@ final _empty = Future.value(null); /// performed at every node in an asynchronous graph, but does not give /// guarantees that the work is done in topological order. /// -/// If [readNode] returns null for any key it will be ignored from the rest of -/// the graph. If missing nodes are important they should be tracked within the -/// [readNode] callback. -/// /// If either [readNode] or [edges] throws the error will be forwarded /// through the result stream and no further nodes will be crawled, though some /// work may have already been started. @@ -54,7 +50,7 @@ class _CrawlAsync { /// Add all nodes in the graph to [result] and return a Future which fires /// after all nodes have been seen. - Future run() async { + Future run() async { try { await Future.wait(roots.map(_visit), eagerError: true); await result.close(); @@ -66,9 +62,8 @@ class _CrawlAsync { /// Resolve the node at [key] and output it, then start crawling all of it's /// edges. - Future _crawlFrom(K key) async { + Future _crawlFrom(K key) async { var value = await readNode(key); - if (value == null) return; if (result.isClosed) return; result.add(value); var next = await edges(key, value); @@ -81,7 +76,7 @@ class _CrawlAsync { /// The returned Future will complete only after the work for [key] and all /// transitively reachable nodes has either been finished, or will be finished /// by some other Future in [_seen]. - Future _visit(K key) { + Future _visit(K key) { if (_seen.contains(key)) return _empty; _seen.add(key); return _crawlFrom(key); diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 5e5dba7e67..bba0476877 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,10 +1,10 @@ name: graphs -version: 1.0.0 +version: 2.0.0-dev description: Graph algorithms that operate on graphs in any representation -homepage: https://github.com/dart-lang/graphs +repository: https://github.com/dart-lang/graphs environment: - sdk: '>=2.12.0-0 <3.0.0' + sdk: '>=2.12.0 <3.0.0' dev_dependencies: pedantic: ^1.10.0 diff --git a/pkgs/graphs/test/crawl_async_test.dart b/pkgs/graphs/test/crawl_async_test.dart index 3cb3d4a8f0..0e9630a0a1 100644 --- a/pkgs/graphs/test/crawl_async_test.dart +++ b/pkgs/graphs/test/crawl_async_test.dart @@ -73,13 +73,13 @@ void main() { expect(result, allOf(contains('a'), contains('b'))); }); - test('ignores null nodes', () async { + test('allows null nodes', () async { var result = await crawl({ 'a': ['b'], }, [ 'a' ]); - expect(result, ['a']); + expect(result, ['a', null]); }); test('surfaces exceptions for crawling edges', () { From 4550d2b8ab635f225cbed555a63ce05e8ef345ad Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Fri, 19 Mar 2021 08:59:50 -0700 Subject: [PATCH 63/88] Prepare to publish version 2.0.0 (#60) --- pkgs/graphs/CHANGELOG.md | 2 +- pkgs/graphs/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index 2515638955..08715860cd 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,4 +1,4 @@ -# 2.0.0-dev +# 2.0.0 - **Breaking**: `crawlAsync` will no longer ignore a node from the graph if the `readNode` callback returns null. diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index bba0476877..4b71e9b6ee 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,5 +1,5 @@ name: graphs -version: 2.0.0-dev +version: 2.0.0 description: Graph algorithms that operate on graphs in any representation repository: https://github.com/dart-lang/graphs From aec8919ee47c2a357c1894d83e8cf6011e6c0b49 Mon Sep 17 00:00:00 2001 From: Franklin Yow <58489007+franklinyow@users.noreply.github.com> Date: Fri, 2 Apr 2021 16:55:20 -0700 Subject: [PATCH 64/88] Update LICENSE (#61) Changes to comply with internal review --- pkgs/graphs/LICENSE | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/LICENSE b/pkgs/graphs/LICENSE index 389ce98563..03af64abe4 100644 --- a/pkgs/graphs/LICENSE +++ b/pkgs/graphs/LICENSE @@ -1,4 +1,5 @@ -Copyright 2017, the Dart project authors. All rights reserved. +Copyright 2017, the Dart project authors. + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -9,7 +10,7 @@ met: copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Google Inc. nor the names of its + * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. From 2cb2e765ed5ea3c12924729daa66460dd7d34786 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 20 Apr 2021 09:25:24 -0700 Subject: [PATCH 65/88] Use latest setup action (#62) --- pkgs/graphs/.github/workflows/ci.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pkgs/graphs/.github/workflows/ci.yml b/pkgs/graphs/.github/workflows/ci.yml index f1bba1676e..b4b947b4d8 100644 --- a/pkgs/graphs/.github/workflows/ci.yml +++ b/pkgs/graphs/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: sdk: [dev] steps: - uses: actions/checkout@v2 - - uses: dart-lang/setup-dart@v0.3 + - uses: dart-lang/setup-dart@v1.0 with: sdk: ${{ matrix.sdk }} - id: install @@ -36,21 +36,17 @@ jobs: run: dart analyze --fatal-infos if: always() && steps.install.outcome == 'success' - # Run tests on a matrix consisting of two dimensions: - # 1. OS: ubuntu-latest, (macos-latest, windows-latest) - # 2. release channel: beta test: needs: analyze runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - # Add macos-latest and/or windows-latest if relevant for this package. os: [ubuntu-latest] sdk: [2.12.0, dev] steps: - uses: actions/checkout@v2 - - uses: dart-lang/setup-dart@v0.3 + - uses: dart-lang/setup-dart@v1.0 with: sdk: ${{ matrix.sdk }} - id: install From 53054c7023aeb8bbd5deeb06ac3f11134acf67ce Mon Sep 17 00:00:00 2001 From: Phil Quitslund Date: Mon, 26 Apr 2021 09:55:24 -0700 Subject: [PATCH 66/88] update analyzer API access (#63) --- pkgs/graphs/example/crawl_async_example.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/example/crawl_async_example.dart b/pkgs/graphs/example/crawl_async_example.dart index d6f3e40f97..8100d52119 100644 --- a/pkgs/graphs/example/crawl_async_example.dart +++ b/pkgs/graphs/example/crawl_async_example.dart @@ -8,6 +8,7 @@ import 'dart:isolate'; import 'package:analyzer/dart/analysis/analysis_context.dart'; import 'package:analyzer/dart/analysis/context_builder.dart'; import 'package:analyzer/dart/analysis/context_locator.dart'; +import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:graphs/graphs.dart'; import 'package:path/path.dart' as p; @@ -58,8 +59,8 @@ Future> findImports(Uri from, Source source) async { Future parseUri(Uri uri) async { var path = await pathForUri(uri); var analysisSession = (await analysisContext).currentSession; - var parseResult = analysisSession.getParsedUnit(path); - return parseResult.unit; + var parseResult = analysisSession.getParsedUnit2(path); + return (parseResult as ParsedUnitResult).unit; } Future pathForUri(Uri uri) async { From c7c7b6c95c8c0b13de0482e969d93b461156f448 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Thu, 15 Jul 2021 17:44:25 -0700 Subject: [PATCH 67/88] Fix CI badge in readme (#64) --- pkgs/graphs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/graphs/README.md b/pkgs/graphs/README.md index 0edaf92dd5..18f203f383 100644 --- a/pkgs/graphs/README.md +++ b/pkgs/graphs/README.md @@ -1,4 +1,4 @@ -# [![Build Status](https://travis-ci.org/dart-lang/graphs.svg?branch=master)](https://travis-ci.org/dart-lang/graphs) +[![CI](https://github.com/dart-lang/graphs/actions/workflows/ci.yml/badge.svg)](https://github.com/dart-lang/graphs/actions/workflows/ci.yml) Graph algorithms which do not specify a particular approach for representing a Graph. From dfbee796c381c5a4dff290c39276e17d87528d38 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Fri, 16 Jul 2021 11:05:02 -0700 Subject: [PATCH 68/88] Update CI (#65) --- pkgs/graphs/.github/workflows/ci.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/pkgs/graphs/.github/workflows/ci.yml b/pkgs/graphs/.github/workflows/ci.yml index b4b947b4d8..ae1edbed27 100644 --- a/pkgs/graphs/.github/workflows/ci.yml +++ b/pkgs/graphs/.github/workflows/ci.yml @@ -27,13 +27,10 @@ jobs: with: sdk: ${{ matrix.sdk }} - id: install - name: Install dependencies run: dart pub get - - name: Check formatting - run: dart format --output=none --set-exit-if-changed . + - run: dart format --output=none --set-exit-if-changed . if: always() && steps.install.outcome == 'success' - - name: Analyze code - run: dart analyze --fatal-infos + - run: dart analyze --fatal-infos if: always() && steps.install.outcome == 'success' test: @@ -50,11 +47,8 @@ jobs: with: sdk: ${{ matrix.sdk }} - id: install - name: Install dependencies - run: pub get - - name: Run VM tests - run: pub run test --platform vm + run: dart pub get + - run: dart test --platform vm if: always() && steps.install.outcome == 'success' - - name: Run Chrome tests - run: pub run test --platform chrome + - run: dart test --platform chrome if: always() && steps.install.outcome == 'success' From e20cc6044daa8cd88ff3d35175213f1d2e5ea872 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 1 Sep 2021 17:10:20 -0700 Subject: [PATCH 69/88] Add a topologicalSort function (#66) --- pkgs/graphs/CHANGELOG.md | 4 + pkgs/graphs/lib/graphs.dart | 2 + pkgs/graphs/lib/src/cycle_exception.dart | 19 +++ pkgs/graphs/lib/src/topological_sort.dart | 56 +++++++++ pkgs/graphs/pubspec.yaml | 5 +- pkgs/graphs/test/topological_sort_test.dart | 128 ++++++++++++++++++++ pkgs/graphs/test/utils/utils.dart | 13 ++ 7 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 pkgs/graphs/lib/src/cycle_exception.dart create mode 100644 pkgs/graphs/lib/src/topological_sort.dart create mode 100644 pkgs/graphs/test/topological_sort_test.dart diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index 08715860cd..8c410d8c00 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,3 +1,7 @@ +# 2.1.0 + +* Add a `topologicalSort()` function. + # 2.0.0 - **Breaking**: `crawlAsync` will no longer ignore a node from the graph if the diff --git a/pkgs/graphs/lib/graphs.dart b/pkgs/graphs/lib/graphs.dart index a5de89cfa7..f272d31979 100644 --- a/pkgs/graphs/lib/graphs.dart +++ b/pkgs/graphs/lib/graphs.dart @@ -3,6 +3,8 @@ // BSD-style license that can be found in the LICENSE file. export 'src/crawl_async.dart' show crawlAsync; +export 'src/cycle_exception.dart' show CycleException; export 'src/shortest_path.dart' show shortestPath, shortestPaths; export 'src/strongly_connected_components.dart' show stronglyConnectedComponents; +export 'src/topological_sort.dart' show topologicalSort; diff --git a/pkgs/graphs/lib/src/cycle_exception.dart b/pkgs/graphs/lib/src/cycle_exception.dart new file mode 100644 index 0000000000..eb9b433e76 --- /dev/null +++ b/pkgs/graphs/lib/src/cycle_exception.dart @@ -0,0 +1,19 @@ +// Copyright (c) 2021, 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. + +/// An exception indicating that a cycle was detected in a graph that was +/// expected to be acyclic. +class CycleException implements Exception { + /// The list of nodes comprising the cycle. + /// + /// Each node in this list has an edge to the next node. The final node has an + /// edge to the first node. + final List cycle; + + CycleException(Iterable cycle) : cycle = List.unmodifiable(cycle); + + @override + String toString() => 'A cycle was detected in a graph that must be acyclic:\n' + '${cycle.map((node) => '* $node').join('\n')}'; +} diff --git a/pkgs/graphs/lib/src/topological_sort.dart b/pkgs/graphs/lib/src/topological_sort.dart new file mode 100644 index 0000000000..d2f345e651 --- /dev/null +++ b/pkgs/graphs/lib/src/topological_sort.dart @@ -0,0 +1,56 @@ +// Copyright (c) 2021, 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. + +import 'dart:collection'; + +import 'package:collection/collection.dart'; + +import 'cycle_exception.dart'; + +/// Returns a topological sort of the nodes of the directed edges of a graph +/// provided by [nodes] and [edges]. +/// +/// Each element of the returned iterable is guaranteed to appear after all +/// nodes that have edges leading to that node. The result is not guaranteed to +/// be unique, nor is it guaranteed to be stable across releases of this +/// package; however, it will be stable for a given input within a given package +/// version. +/// +/// If [equals] is provided, it is used to compare nodes in the graph. If +/// [equals] is omitted, the node's own [Object.==] is used instead. +/// +/// Similarly, if [hashCode] is provided, it is used to produce a hash value +/// for nodes to efficiently calculate the return value. If it is omitted, the +/// key's own [Object.hashCode] is used. +/// +/// If you supply one of [equals] or [hashCode], you should generally also to +/// supply the other. +/// +/// Throws a [CycleException] if the graph is cyclical. +List topologicalSort(Iterable nodes, Iterable Function(T) edges, + {bool Function(T, T)? equals, int Function(T)? hashCode}) { + // https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search + var result = QueueList(); + var permanentMark = HashSet(equals: equals, hashCode: hashCode); + var temporaryMark = LinkedHashSet(equals: equals, hashCode: hashCode); + void visit(T node) { + if (permanentMark.contains(node)) return; + if (temporaryMark.contains(node)) { + throw CycleException(temporaryMark); + } + + temporaryMark.add(node); + for (var child in edges(node)) { + visit(child); + } + temporaryMark.remove(node); + permanentMark.add(node); + result.addFirst(node); + } + + for (var node in nodes) { + visit(node); + } + return result; +} diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 4b71e9b6ee..d2adb42bb1 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,11 +1,14 @@ name: graphs -version: 2.0.0 +version: 2.1.0 description: Graph algorithms that operate on graphs in any representation repository: https://github.com/dart-lang/graphs environment: sdk: '>=2.12.0 <3.0.0' +dependencies: + collection: ^1.1.0 + dev_dependencies: pedantic: ^1.10.0 test: ^1.16.0 diff --git a/pkgs/graphs/test/topological_sort_test.dart b/pkgs/graphs/test/topological_sort_test.dart new file mode 100644 index 0000000000..c775eb2ac8 --- /dev/null +++ b/pkgs/graphs/test/topological_sort_test.dart @@ -0,0 +1,128 @@ +// 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. + +import 'dart:collection'; + +import 'package:graphs/graphs.dart'; +import 'package:test/test.dart'; + +import 'utils/utils.dart'; + +void main() { + group('sorts a graph', () { + test('with no nodes', () { + expect(_topologicalSort({}), isEmpty); + }); + + test('with only one node', () { + expect(_topologicalSort({1: []}), equals([1])); + }); + + test('with no edges', () { + expect(_topologicalSort({1: [], 2: [], 3: [], 4: []}), + unorderedEquals([1, 2, 3, 4])); + }); + + test('with single edges', () { + expect( + _topologicalSort({ + 1: [2], + 2: [3], + 3: [4], + 4: [] + }), + equals([1, 2, 3, 4])); + }); + + test('with many edges from one node', () { + var result = _topologicalSort({ + 1: [2, 3, 4], + 2: [], + 3: [], + 4: [] + }); + expect(result.indexOf(1), lessThan(result.indexOf(2))); + expect(result.indexOf(1), lessThan(result.indexOf(3))); + expect(result.indexOf(1), lessThan(result.indexOf(4))); + }); + + test('with transitive edges', () { + var result = _topologicalSort({ + 1: [2, 4], + 2: [], + 3: [], + 4: [3] + }); + expect(result.indexOf(1), lessThan(result.indexOf(2))); + expect(result.indexOf(1), lessThan(result.indexOf(3))); + expect(result.indexOf(1), lessThan(result.indexOf(4))); + expect(result.indexOf(4), lessThan(result.indexOf(3))); + }); + + test('with diamond edges', () { + var result = _topologicalSort({ + 1: [2, 3], + 2: [4], + 3: [4], + 4: [] + }); + expect(result.indexOf(1), lessThan(result.indexOf(2))); + expect(result.indexOf(1), lessThan(result.indexOf(3))); + expect(result.indexOf(1), lessThan(result.indexOf(4))); + expect(result.indexOf(2), lessThan(result.indexOf(4))); + expect(result.indexOf(3), lessThan(result.indexOf(4))); + }); + }); + + test('respects custom equality and hash functions', () { + expect( + _topologicalSort({ + 0: [2], + 3: [4], + 5: [6], + 7: [] + }, + equals: (i, j) => (i ~/ 2) == (j ~/ 2), + hashCode: (i) => (i ~/ 2).hashCode), + equals([ + 0, + anyOf([2, 3]), + anyOf([4, 5]), + anyOf([6, 7]) + ])); + }); + + group('throws a CycleException for a graph with', () { + test('a one-node cycle', () { + expect( + () => _topologicalSort({ + 1: [1] + }), + throwsCycleException([1])); + }); + + test('a multi-node cycle', () { + expect( + () => _topologicalSort({ + 1: [2], + 2: [3], + 3: [4], + 4: [1] + }), + throwsCycleException([1, 2, 3, 4])); + }); + }); +} + +/// Runs a topological sort on a graph represented a map from keys to edges. +List _topologicalSort(Map> graph, + {bool Function(T, T)? equals, int Function(T)? hashCode}) { + if (equals != null) { + graph = LinkedHashMap(equals: equals, hashCode: hashCode)..addAll(graph); + } + return topologicalSort(graph.keys, (node) { + expect(graph, contains(node)); + return graph[node]!; + }, equals: equals, hashCode: hashCode); +} diff --git a/pkgs/graphs/test/utils/utils.dart b/pkgs/graphs/test/utils/utils.dart index 19853e75e0..4703843f7a 100644 --- a/pkgs/graphs/test/utils/utils.dart +++ b/pkgs/graphs/test/utils/utils.dart @@ -2,10 +2,23 @@ // 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. +import 'package:graphs/graphs.dart'; +import 'package:test/test.dart'; + bool xEquals(X a, X b) => a.value == b.value; int xHashCode(X a) => a.value.hashCode; +/// Returns a matcher that verifies that a function throws a [CycleException] +/// with the given [cycle]. +Matcher throwsCycleException(List cycle) => throwsA(allOf([ + isA>(), + predicate((exception) { + expect((exception as CycleException).cycle, equals(cycle)); + return true; + }) + ])); + class X { final String value; From 1437a7d560b3d2f7be59b319b53d39d63f182acc Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Mon, 4 Oct 2021 08:49:51 -0700 Subject: [PATCH 70/88] Bump analyzer dep, use pkg:lints, fix lints (#68) --- pkgs/graphs/CHANGELOG.md | 2 ++ pkgs/graphs/analysis_options.yaml | 4 +++- .../benchmark/shortest_path_worst_case_benchmark.dart | 2 +- pkgs/graphs/example/crawl_async_example.dart | 4 ++-- pkgs/graphs/pubspec.yaml | 8 ++++---- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index 8c410d8c00..2b92a68523 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,3 +1,5 @@ +# 2.1.1-dev + # 2.1.0 * Add a `topologicalSort()` function. diff --git a/pkgs/graphs/analysis_options.yaml b/pkgs/graphs/analysis_options.yaml index 25de3dcade..5dfd97adde 100644 --- a/pkgs/graphs/analysis_options.yaml +++ b/pkgs/graphs/analysis_options.yaml @@ -1,9 +1,11 @@ -include: package:pedantic/analysis_options.yaml +include: package:lints/recommended.yaml + analyzer: strong-mode: implicit-casts: false language: strict-inference: true + linter: rules: - avoid_function_literals_in_foreach_calls diff --git a/pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart b/pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart index cbacf62352..02f7140b28 100644 --- a/pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart +++ b/pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart @@ -29,7 +29,7 @@ void main() { shortestPath(0, size - 1, (e) => graph[e] ?? []).toString(); print(testOutput); assert(testOutput == Iterable.generate(size - 1, (i) => i + 1).toString(), - '$testOutput'); + testOutput); final watch = Stopwatch(); for (var i = 1;; i++) { diff --git a/pkgs/graphs/example/crawl_async_example.dart b/pkgs/graphs/example/crawl_async_example.dart index 8100d52119..a6688dc7fd 100644 --- a/pkgs/graphs/example/crawl_async_example.dart +++ b/pkgs/graphs/example/crawl_async_example.dart @@ -16,7 +16,7 @@ import 'package:pool/pool.dart'; /// Print a transitive set of imported URIs where libraries are read /// asynchronously. -Future main() async { +Future main() async { // Limits calls to [findImports]. var _pool = Pool(10); var allImports = await crawlAsync( @@ -59,7 +59,7 @@ Future> findImports(Uri from, Source source) async { Future parseUri(Uri uri) async { var path = await pathForUri(uri); var analysisSession = (await analysisContext).currentSession; - var parseResult = analysisSession.getParsedUnit2(path); + var parseResult = analysisSession.getParsedUnit(path); return (parseResult as ParsedUnitResult).unit; } diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index d2adb42bb1..8daef9307c 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,19 +1,19 @@ name: graphs -version: 2.1.0 +version: 2.1.1-dev description: Graph algorithms that operate on graphs in any representation repository: https://github.com/dart-lang/graphs environment: sdk: '>=2.12.0 <3.0.0' -dependencies: +dependencies: collection: ^1.1.0 dev_dependencies: - pedantic: ^1.10.0 + lints: ^1.0.0 test: ^1.16.0 # For examples path: ^1.8.0 pool: ^1.5.0 - analyzer: ^1.0.0 + analyzer: ^2.0.0 From 9fe5d2b7d77625af2c6ed5e6858c1f95a3a5defc Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Mon, 17 Jan 2022 18:51:08 -0800 Subject: [PATCH 71/88] Support latest pkg:analyzer (#69) --- pkgs/graphs/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 8daef9307c..9279ebd0cd 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -16,4 +16,4 @@ dev_dependencies: # For examples path: ^1.8.0 pool: ^1.5.0 - analyzer: ^2.0.0 + analyzer: '>=2.0.0 <4.0.0' From 2f1fbdccb555a830a5771a82c3e20a52ff63e341 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Tue, 8 Feb 2022 09:06:30 -0800 Subject: [PATCH 72/88] Remove leading underscore from local var (#72) Closes #41 --- pkgs/graphs/example/crawl_async_example.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/example/crawl_async_example.dart b/pkgs/graphs/example/crawl_async_example.dart index a6688dc7fd..002b32d27c 100644 --- a/pkgs/graphs/example/crawl_async_example.dart +++ b/pkgs/graphs/example/crawl_async_example.dart @@ -18,11 +18,11 @@ import 'package:pool/pool.dart'; /// asynchronously. Future main() async { // Limits calls to [findImports]. - var _pool = Pool(10); + var pool = Pool(10); var allImports = await crawlAsync( [Uri.parse('package:graphs/graphs.dart')], read, - (from, source) => _pool.withResource(() => findImports(from, source))) + (from, source) => pool.withResource(() => findImports(from, source))) .toList(); print(allImports.map((s) => s.uri).toList()); } From 5985a8867c5e3d4d07f66080e88aa401cc54d31f Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 8 Feb 2022 16:32:08 -0800 Subject: [PATCH 73/88] Add a `secondarySort` parameter to `topologicalSort` (#70) This makes it possible to enforce a topological sort but ensure that within that, existing nodes are sorted lexically as much as possible. --- pkgs/graphs/CHANGELOG.md | 6 +- pkgs/graphs/lib/src/topological_sort.dart | 75 ++++- pkgs/graphs/pubspec.yaml | 2 +- pkgs/graphs/test/topological_sort_test.dart | 350 +++++++++++++++----- 4 files changed, 344 insertions(+), 89 deletions(-) diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index 2b92a68523..decfa84709 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,4 +1,8 @@ -# 2.1.1-dev +# 2.2.0 + +* Add a `secondarySort` parameter to the `topologicalSort()` function which + applies an additional lexical sort where that doesn't break the topological + sort. # 2.1.0 diff --git a/pkgs/graphs/lib/src/topological_sort.dart b/pkgs/graphs/lib/src/topological_sort.dart index d2f345e651..40c09edf61 100644 --- a/pkgs/graphs/lib/src/topological_sort.dart +++ b/pkgs/graphs/lib/src/topological_sort.dart @@ -4,7 +4,7 @@ import 'dart:collection'; -import 'package:collection/collection.dart'; +import 'package:collection/collection.dart' hide stronglyConnectedComponents; import 'cycle_exception.dart'; @@ -27,9 +27,24 @@ import 'cycle_exception.dart'; /// If you supply one of [equals] or [hashCode], you should generally also to /// supply the other. /// +/// If you supply [secondarySort], the resulting list will be sorted by that +/// comparison function as much as possible without violating the topological +/// ordering. Note that even with a secondary sort, the result is _still_ not +/// guaranteed to be unique or stable across releases of this package. +/// +/// Note: this requires that [nodes] and each iterable returned by [edges] +/// contain no duplicate entries. +/// /// Throws a [CycleException] if the graph is cyclical. List topologicalSort(Iterable nodes, Iterable Function(T) edges, - {bool Function(T, T)? equals, int Function(T)? hashCode}) { + {bool Function(T, T)? equals, + int Function(T)? hashCode, + Comparator? secondarySort}) { + if (secondarySort != null) { + return _topologicalSortWithSecondary( + [...nodes], edges, secondarySort, equals, hashCode); + } + // https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search var result = QueueList(); var permanentMark = HashSet(equals: equals, hashCode: hashCode); @@ -54,3 +69,59 @@ List topologicalSort(Iterable nodes, Iterable Function(T) edges, } return result; } + +/// An implementation of [topologicalSort] with a secondary comparison function. +List _topologicalSortWithSecondary( + List nodes, + Iterable Function(T) edges, + Comparator comparator, + bool Function(T, T)? equals, + int Function(T)? hashCode) { + // https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm, + // modified to sort the nodes to traverse. Also documented in + // https://www.algotree.org/algorithms/tree_graph_traversal/lexical_topological_sort_c++/ + + // For each node, the number of incoming edges it has that we haven't yet + // traversed. + var incomingEdges = HashMap(equals: equals, hashCode: hashCode); + for (var node in nodes) { + for (var child in edges(node)) { + incomingEdges[child] = (incomingEdges[child] ?? 0) + 1; + } + } + + // A priority queue of nodes that have no remaining incoming edges. + var nodesToTraverse = PriorityQueue(comparator); + for (var node in nodes) { + if (!incomingEdges.containsKey(node)) nodesToTraverse.add(node); + } + + var result = []; + while (nodesToTraverse.isNotEmpty) { + var node = nodesToTraverse.removeFirst(); + result.add(node); + for (var child in edges(node)) { + var remainingEdges = incomingEdges[child]!; + remainingEdges--; + incomingEdges[child] = remainingEdges; + if (remainingEdges == 0) nodesToTraverse.add(child); + } + } + + if (result.length < nodes.length) { + // This algorithm doesn't automatically produce a cycle list as a side + // effect of sorting, so to throw the appropriate [CycleException] we just + // call the normal [topologicalSort] with a view of this graph that only + // includes nodes that still have edges. + bool nodeIsInCycle(T node) { + var edges = incomingEdges[node]; + return edges != null && edges > 0; + } + + topologicalSort(nodes.where(nodeIsInCycle), edges, + equals: equals, hashCode: hashCode); + assert(false, 'topologicalSort() should throw if the graph has a cycle'); + } + + return result; +} diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 9279ebd0cd..2e3407534c 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,5 +1,5 @@ name: graphs -version: 2.1.1-dev +version: 2.2.0 description: Graph algorithms that operate on graphs in any representation repository: https://github.com/dart-lang/graphs diff --git a/pkgs/graphs/test/topological_sort_test.dart b/pkgs/graphs/test/topological_sort_test.dart index c775eb2ac8..3870d5a1bc 100644 --- a/pkgs/graphs/test/topological_sort_test.dart +++ b/pkgs/graphs/test/topological_sort_test.dart @@ -10,119 +10,299 @@ import 'package:test/test.dart'; import 'utils/utils.dart'; void main() { - group('sorts a graph', () { - test('with no nodes', () { - expect(_topologicalSort({}), isEmpty); - }); + group('without secondarySort', () { + group('topologically sorts a graph', () { + test('with no nodes', () { + expect(_topologicalSort({}), isEmpty); + }); - test('with only one node', () { - expect(_topologicalSort({1: []}), equals([1])); - }); + test('with only one node', () { + expect(_topologicalSort({1: []}), equals([1])); + }); - test('with no edges', () { - expect(_topologicalSort({1: [], 2: [], 3: [], 4: []}), - unorderedEquals([1, 2, 3, 4])); + test('with no edges', () { + expect(_topologicalSort({1: [], 2: [], 3: [], 4: []}), + unorderedEquals([1, 2, 3, 4])); + }); + + test('with single edges', () { + expect( + _topologicalSort({ + 1: [2], + 2: [3], + 3: [4], + 4: [] + }), + equals([1, 2, 3, 4])); + }); + + test('with many edges from one node', () { + var result = _topologicalSort({ + 1: [2, 3, 4], + 2: [], + 3: [], + 4: [] + }); + expect(result.indexOf(1), lessThan(result.indexOf(2))); + expect(result.indexOf(1), lessThan(result.indexOf(3))); + expect(result.indexOf(1), lessThan(result.indexOf(4))); + }); + + test('with transitive edges', () { + var result = _topologicalSort({ + 1: [2, 4], + 2: [], + 3: [], + 4: [3] + }); + expect(result.indexOf(1), lessThan(result.indexOf(2))); + expect(result.indexOf(1), lessThan(result.indexOf(3))); + expect(result.indexOf(1), lessThan(result.indexOf(4))); + expect(result.indexOf(4), lessThan(result.indexOf(3))); + }); + + test('with diamond edges', () { + var result = _topologicalSort({ + 1: [2, 3], + 2: [4], + 3: [4], + 4: [] + }); + expect(result.indexOf(1), lessThan(result.indexOf(2))); + expect(result.indexOf(1), lessThan(result.indexOf(3))); + expect(result.indexOf(1), lessThan(result.indexOf(4))); + expect(result.indexOf(2), lessThan(result.indexOf(4))); + expect(result.indexOf(3), lessThan(result.indexOf(4))); + }); }); - test('with single edges', () { + test('respects custom equality and hash functions', () { expect( - _topologicalSort({ - 1: [2], - 2: [3], + _topologicalSort({ + 0: [2], 3: [4], - 4: [] - }), - equals([1, 2, 3, 4])); + 5: [6], + 7: [] + }, + equals: (i, j) => (i ~/ 2) == (j ~/ 2), + hashCode: (i) => (i ~/ 2).hashCode), + equals([ + 0, + anyOf([2, 3]), + anyOf([4, 5]), + anyOf([6, 7]) + ])); }); - test('with many edges from one node', () { - var result = _topologicalSort({ - 1: [2, 3, 4], - 2: [], - 3: [], - 4: [] + group('throws a CycleException for a graph with', () { + test('a one-node cycle', () { + expect( + () => _topologicalSort({ + 1: [1] + }), + throwsCycleException([1])); }); - expect(result.indexOf(1), lessThan(result.indexOf(2))); - expect(result.indexOf(1), lessThan(result.indexOf(3))); - expect(result.indexOf(1), lessThan(result.indexOf(4))); - }); - test('with transitive edges', () { - var result = _topologicalSort({ - 1: [2, 4], - 2: [], - 3: [], - 4: [3] - }); - expect(result.indexOf(1), lessThan(result.indexOf(2))); - expect(result.indexOf(1), lessThan(result.indexOf(3))); - expect(result.indexOf(1), lessThan(result.indexOf(4))); - expect(result.indexOf(4), lessThan(result.indexOf(3))); - }); - - test('with diamond edges', () { - var result = _topologicalSort({ - 1: [2, 3], - 2: [4], - 3: [4], - 4: [] - }); - expect(result.indexOf(1), lessThan(result.indexOf(2))); - expect(result.indexOf(1), lessThan(result.indexOf(3))); - expect(result.indexOf(1), lessThan(result.indexOf(4))); - expect(result.indexOf(2), lessThan(result.indexOf(4))); - expect(result.indexOf(3), lessThan(result.indexOf(4))); + test('a multi-node cycle', () { + expect( + () => _topologicalSort({ + 1: [2], + 2: [3], + 3: [4], + 4: [1] + }), + throwsCycleException([1, 2, 3, 4])); + }); }); }); - test('respects custom equality and hash functions', () { - expect( - _topologicalSort({ - 0: [2], + group('with secondarySort', () { + group('topologically sorts a graph', () { + test('with no nodes', () { + expect(_topologicalSort({}, secondarySort: true), isEmpty); + }); + + test('with only one node', () { + expect(_topologicalSort({1: []}, secondarySort: true), equals([1])); + }); + + test('with no edges', () { + expect( + _topologicalSort({1: [], 2: [], 3: [], 4: []}, secondarySort: true), + unorderedEquals([1, 2, 3, 4])); + }); + + test('with single edges', () { + expect( + _topologicalSort({ + 1: [2], + 2: [3], + 3: [4], + 4: [] + }, secondarySort: true), + equals([1, 2, 3, 4])); + }); + + test('with many edges from one node', () { + var result = _topologicalSort({ + 1: [2, 3, 4], + 2: [], + 3: [], + 4: [] + }, secondarySort: true); + expect(result.indexOf(1), lessThan(result.indexOf(2))); + expect(result.indexOf(1), lessThan(result.indexOf(3))); + expect(result.indexOf(1), lessThan(result.indexOf(4))); + }); + + test('with transitive edges', () { + var result = _topologicalSort({ + 1: [2, 4], + 2: [], + 3: [], + 4: [3] + }, secondarySort: true); + expect(result.indexOf(1), lessThan(result.indexOf(2))); + expect(result.indexOf(1), lessThan(result.indexOf(3))); + expect(result.indexOf(1), lessThan(result.indexOf(4))); + expect(result.indexOf(4), lessThan(result.indexOf(3))); + }); + + test('with diamond edges', () { + var result = _topologicalSort({ + 1: [2, 3], + 2: [4], 3: [4], - 5: [6], - 7: [] - }, - equals: (i, j) => (i ~/ 2) == (j ~/ 2), - hashCode: (i) => (i ~/ 2).hashCode), - equals([ - 0, - anyOf([2, 3]), - anyOf([4, 5]), - anyOf([6, 7]) - ])); - }); + 4: [] + }, secondarySort: true); + expect(result.indexOf(1), lessThan(result.indexOf(2))); + expect(result.indexOf(1), lessThan(result.indexOf(3))); + expect(result.indexOf(1), lessThan(result.indexOf(4))); + expect(result.indexOf(2), lessThan(result.indexOf(4))); + expect(result.indexOf(3), lessThan(result.indexOf(4))); + }); + }); - group('throws a CycleException for a graph with', () { - test('a one-node cycle', () { - expect( - () => _topologicalSort({ - 1: [1] - }), - throwsCycleException([1])); + group('lexically sorts a graph where possible', () { + test('with no edges', () { + var result = + _topologicalSort({4: [], 3: [], 1: [], 2: []}, secondarySort: true); + expect(result, equals([1, 2, 3, 4])); + }); + + test('with one non-lexical edge', () { + var result = _topologicalSort({ + 4: [], + 3: [1], + 1: [], + 2: [] + }, secondarySort: true); + expect( + result, + equals(anyOf([ + [2, 3, 1, 4], + [3, 1, 2, 4] + ]))); + }); + + test('with a non-lexical topolgical order', () { + var result = _topologicalSort({ + 4: [3], + 3: [2], + 2: [1], + 1: [] + }, secondarySort: true); + expect(result, equals([4, 3, 2, 1])); + }); + + group('with multiple layers', () { + test('in lexical order', () { + var result = _topologicalSort({ + 1: [2], + 2: [3], + 3: [], + 4: [5], + 5: [6], + 6: [] + }, secondarySort: true); + expect(result, equals([1, 2, 3, 4, 5, 6])); + }); + + test('in non-lexical order', () { + var result = _topologicalSort({ + 1: [3], + 3: [5], + 4: [2], + 2: [6], + 5: [], + 6: [] + }, secondarySort: true); + expect( + result, + anyOf([ + equals([1, 3, 4, 2, 5, 6]), + equals([1, 4, 2, 3, 5, 6]) + ])); + }); + }); }); - test('a multi-node cycle', () { + test('respects custom equality and hash functions', () { expect( - () => _topologicalSort({ - 1: [2], - 2: [3], - 3: [4], - 4: [1] - }), - throwsCycleException([1, 2, 3, 4])); + _topologicalSort({ + 0: [2], + 3: [4], + 5: [6], + 7: [] + }, + equals: (i, j) => (i ~/ 2) == (j ~/ 2), + hashCode: (i) => (i ~/ 2).hashCode, + secondarySort: true), + equals([ + 0, + anyOf([2, 3]), + anyOf([4, 5]), + anyOf([6, 7]) + ])); + }); + + group('throws a CycleException for a graph with', () { + test('a one-node cycle', () { + expect( + () => _topologicalSort({ + 1: [1] + }, secondarySort: true), + throwsCycleException([1])); + }); + + test('a multi-node cycle', () { + expect( + () => _topologicalSort({ + 1: [2], + 2: [3], + 3: [4], + 4: [1] + }, secondarySort: true), + throwsCycleException([1, 2, 3, 4])); + }); }); }); } /// Runs a topological sort on a graph represented a map from keys to edges. List _topologicalSort(Map> graph, - {bool Function(T, T)? equals, int Function(T)? hashCode}) { + {bool Function(T, T)? equals, + int Function(T)? hashCode, + bool secondarySort = false}) { if (equals != null) { graph = LinkedHashMap(equals: equals, hashCode: hashCode)..addAll(graph); } return topologicalSort(graph.keys, (node) { expect(graph, contains(node)); return graph[node]!; - }, equals: equals, hashCode: hashCode); + }, + equals: equals, + hashCode: hashCode, + secondarySort: + secondarySort ? (a, b) => (a as Comparable).compareTo(b) : null); } From e22a6acf3739045b5b9a238c3818fa06f4b874e8 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Wed, 20 Apr 2022 09:43:06 -0700 Subject: [PATCH 74/88] update analyzer dependency (#73) --- pkgs/graphs/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 2e3407534c..5c3d7a011f 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -14,6 +14,6 @@ dev_dependencies: test: ^1.16.0 # For examples + analyzer: '>=2.0.0 <5.0.0' path: ^1.8.0 pool: ^1.5.0 - analyzer: '>=2.0.0 <4.0.0' From c69ed05e22c2c1c602d461096c6472dfb96f3a15 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Mon, 25 Jul 2022 13:30:49 -0700 Subject: [PATCH 75/88] Remove deprecated experimental invariant_booleans lint rule (#74) --- pkgs/graphs/analysis_options.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/pkgs/graphs/analysis_options.yaml b/pkgs/graphs/analysis_options.yaml index 5dfd97adde..8ac244a44f 100644 --- a/pkgs/graphs/analysis_options.yaml +++ b/pkgs/graphs/analysis_options.yaml @@ -21,7 +21,6 @@ linter: - empty_statements - hash_and_equals - implementation_imports - - invariant_booleans - iterable_contains_unrelated_type - list_remove_unrelated_type - no_adjacent_strings_in_list From ad6ec06eb35e6f2c3d958836b94a8e87e284e069 Mon Sep 17 00:00:00 2001 From: Jay Gajjar Date: Fri, 21 Oct 2022 22:01:27 +0530 Subject: [PATCH 76/88] Update README.md (#67) --- pkgs/graphs/README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkgs/graphs/README.md b/pkgs/graphs/README.md index 18f203f383..235cef8908 100644 --- a/pkgs/graphs/README.md +++ b/pkgs/graphs/README.md @@ -1,6 +1,6 @@ [![CI](https://github.com/dart-lang/graphs/actions/workflows/ci.yml/badge.svg)](https://github.com/dart-lang/graphs/actions/workflows/ci.yml) -Graph algorithms which do not specify a particular approach for representing a +Graph algorithms that do not specify a particular approach for representing a Graph. Functions in this package will take arguments that provide the mechanism for @@ -31,11 +31,10 @@ Any representation can be adapted to the needs of the algorithm: - Some algorithms need to associate data with each node in the graph. If the node type `T` does not correctly or efficiently implement `hashCode` or `==`, you may provide optional `equals` and/or `hashCode` functions are parameters. -- Algorithms which need to traverse the graph take a `edges` function which - provides the reachable nodes. +- Algorithms which need to traverse the graph take a `edges` function which provides the reachable nodes. - `(node) => graph[node]` - `(node) => node.edges` -Graphs which are resolved asynchronously will have similar functions which +Graphs that are resolved asynchronously will have similar functions which return `FutureOr`. From d7a5c4142f95f4634f4d2e53421a0641ae676f26 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Fri, 21 Oct 2022 16:41:18 +0000 Subject: [PATCH 77/88] update the CI configuration and add markdown badges (#75) --- pkgs/graphs/.github/dependabot.yaml | 8 ++++++++ pkgs/graphs/.github/workflows/ci.yml | 8 ++++---- pkgs/graphs/README.md | 2 ++ 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 pkgs/graphs/.github/dependabot.yaml diff --git a/pkgs/graphs/.github/dependabot.yaml b/pkgs/graphs/.github/dependabot.yaml new file mode 100644 index 0000000000..214481934b --- /dev/null +++ b/pkgs/graphs/.github/dependabot.yaml @@ -0,0 +1,8 @@ +# Dependabot configuration file. +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/pkgs/graphs/.github/workflows/ci.yml b/pkgs/graphs/.github/workflows/ci.yml index ae1edbed27..2f31fe3959 100644 --- a/pkgs/graphs/.github/workflows/ci.yml +++ b/pkgs/graphs/.github/workflows/ci.yml @@ -22,8 +22,8 @@ jobs: matrix: sdk: [dev] steps: - - uses: actions/checkout@v2 - - uses: dart-lang/setup-dart@v1.0 + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + - uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d with: sdk: ${{ matrix.sdk }} - id: install @@ -42,8 +42,8 @@ jobs: os: [ubuntu-latest] sdk: [2.12.0, dev] steps: - - uses: actions/checkout@v2 - - uses: dart-lang/setup-dart@v1.0 + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + - uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d with: sdk: ${{ matrix.sdk }} - id: install diff --git a/pkgs/graphs/README.md b/pkgs/graphs/README.md index 235cef8908..a47ca8d2e0 100644 --- a/pkgs/graphs/README.md +++ b/pkgs/graphs/README.md @@ -1,4 +1,6 @@ [![CI](https://github.com/dart-lang/graphs/actions/workflows/ci.yml/badge.svg)](https://github.com/dart-lang/graphs/actions/workflows/ci.yml) +[![pub package](https://img.shields.io/pub/v/graphs.svg)](https://pub.dev/packages/graphs) +[![package publisher](https://img.shields.io/pub/publisher/graphs.svg)](https://pub.dev/packages/graphs/publisher) Graph algorithms that do not specify a particular approach for representing a Graph. From 241ac3f9902d857debf7f1adcd3ec3791b4844f8 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Tue, 1 Nov 2022 09:47:00 -0700 Subject: [PATCH 78/88] Bump pkg:lints, bump mind Dart SDK and pkg:analyzer (#76) Enable and fix lints --- pkgs/graphs/.github/workflows/ci.yml | 2 +- pkgs/graphs/CHANGELOG.md | 4 + pkgs/graphs/analysis_options.yaml | 57 ++-- .../connected_components_benchmark.dart | 10 +- .../benchmark/shortest_path_benchmark.dart | 8 +- .../shortest_path_worst_case_benchmark.dart | 8 +- pkgs/graphs/example/crawl_async_example.dart | 41 ++- pkgs/graphs/example/example.dart | 16 +- pkgs/graphs/lib/src/crawl_async.dart | 11 +- pkgs/graphs/lib/src/shortest_path.dart | 4 +- .../src/strongly_connected_components.dart | 2 +- pkgs/graphs/lib/src/topological_sort.dart | 53 +-- pkgs/graphs/pubspec.yaml | 8 +- pkgs/graphs/test/crawl_async_test.dart | 44 ++- pkgs/graphs/test/shortest_path_test.dart | 58 ++-- .../strongly_connected_components_test.dart | 161 +++++----- pkgs/graphs/test/topological_sort_test.dart | 304 +++++++++++------- pkgs/graphs/test/utils/graph.dart | 6 +- pkgs/graphs/test/utils/utils.dart | 16 +- 19 files changed, 468 insertions(+), 345 deletions(-) diff --git a/pkgs/graphs/.github/workflows/ci.yml b/pkgs/graphs/.github/workflows/ci.yml index 2f31fe3959..272ce6e027 100644 --- a/pkgs/graphs/.github/workflows/ci.yml +++ b/pkgs/graphs/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - sdk: [2.12.0, dev] + sdk: [2.18.0, dev] steps: - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index decfa84709..74718101aa 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,3 +1,7 @@ +# 2.2.1-dev + +- Require Dart 2.18 + # 2.2.0 * Add a `secondarySort` parameter to the `topologicalSort()` function which diff --git a/pkgs/graphs/analysis_options.yaml b/pkgs/graphs/analysis_options.yaml index 8ac244a44f..5e04335ab7 100644 --- a/pkgs/graphs/analysis_options.yaml +++ b/pkgs/graphs/analysis_options.yaml @@ -1,43 +1,56 @@ +# https://dart.dev/guides/language/analysis-options include: package:lints/recommended.yaml analyzer: - strong-mode: - implicit-casts: false language: + strict-casts: true strict-inference: true + strict-raw-types: true linter: rules: - - avoid_function_literals_in_foreach_calls + - always_declare_return_types + - avoid_bool_literals_in_conditional_expressions + - avoid_catching_errors + - avoid_classes_with_only_static_members + - avoid_dynamic_calls + - avoid_private_typedef_functions + - avoid_redundant_argument_values - avoid_returning_null + - avoid_returning_null_for_future + - avoid_returning_this - avoid_unused_constructor_parameters - - await_only_futures - - camel_case_types + - avoid_void_async - cancel_subscriptions - comment_references - - constant_identifier_names - - control_flow_in_finally - directives_ordering - - empty_statements - - hash_and_equals - - implementation_imports - - iterable_contains_unrelated_type - - list_remove_unrelated_type + - join_return_with_assignment + - lines_longer_than_80_chars + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings - no_adjacent_strings_in_list - - non_constant_identifier_names + - no_runtimeType_toString + - omit_local_variable_types - only_throw_errors - - overridden_fields - package_api_docs - - package_names - - package_prefixed_library_names + - prefer_asserts_in_initializer_lists - prefer_const_constructors - - prefer_initializing_formals - - prefer_interpolation_to_compose_strings - - prefer_typing_uninitialized_variables + - prefer_const_declarations + - prefer_expression_function_bodies + - prefer_final_locals + - prefer_relative_imports + - prefer_single_quotes + - require_trailing_commas - test_types_in_equals - throw_in_finally - - unnecessary_brace_in_string_interps - - unnecessary_getters_setters + - type_annotate_public_apis + - unawaited_futures + - unnecessary_await_in_return - unnecessary_lambdas - - unnecessary_null_aware_assignments + - unnecessary_parenthesis + - unnecessary_raw_strings - unnecessary_statements + - use_if_null_to_convert_nulls_to_bools + - use_raw_strings + - use_string_buffers + - use_super_parameters diff --git a/pkgs/graphs/benchmark/connected_components_benchmark.dart b/pkgs/graphs/benchmark/connected_components_benchmark.dart index 16a15d0d0d..1f7187922c 100644 --- a/pkgs/graphs/benchmark/connected_components_benchmark.dart +++ b/pkgs/graphs/benchmark/connected_components_benchmark.dart @@ -8,14 +8,14 @@ import 'dart:math' show Random; import 'package:graphs/graphs.dart'; void main() { - final _rnd = Random(0); - final size = 2000; + final rnd = Random(0); + const size = 2000; final graph = HashMap>(); for (var i = 0; i < size * 3; i++) { - final toList = graph.putIfAbsent(_rnd.nextInt(size), () => []); + final toList = graph.putIfAbsent(rnd.nextInt(size), () => []); - final toValue = _rnd.nextInt(size); + final toValue = rnd.nextInt(size); if (!toList.contains(toValue)) { toList.add(toValue); } @@ -24,7 +24,7 @@ void main() { var maxCount = 0; var maxIteration = 0; - final duration = const Duration(milliseconds: 100); + const duration = Duration(milliseconds: 100); for (var i = 1;; i++) { var count = 0; diff --git a/pkgs/graphs/benchmark/shortest_path_benchmark.dart b/pkgs/graphs/benchmark/shortest_path_benchmark.dart index c06dc2c4c0..67e7367c5d 100644 --- a/pkgs/graphs/benchmark/shortest_path_benchmark.dart +++ b/pkgs/graphs/benchmark/shortest_path_benchmark.dart @@ -8,14 +8,14 @@ import 'dart:math' show Random; import 'package:graphs/graphs.dart'; void main() { - final _rnd = Random(1); - final size = 1000; + final rnd = Random(1); + const size = 1000; final graph = HashMap>(); for (var i = 0; i < size * 5; i++) { - final toList = graph.putIfAbsent(_rnd.nextInt(size), () => []); + final toList = graph.putIfAbsent(rnd.nextInt(size), () => []); - final toValue = _rnd.nextInt(size); + final toValue = rnd.nextInt(size); if (!toList.contains(toValue)) { toList.add(toValue); } diff --git a/pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart b/pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart index 02f7140b28..d3117acd2e 100644 --- a/pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart +++ b/pkgs/graphs/benchmark/shortest_path_worst_case_benchmark.dart @@ -7,7 +7,7 @@ import 'dart:collection'; import 'package:graphs/graphs.dart'; void main() { - final size = 1000; + const size = 1000; final graph = HashMap>(); // We create a graph where every subsequent node has an edge to every other @@ -28,8 +28,10 @@ void main() { final testOutput = shortestPath(0, size - 1, (e) => graph[e] ?? []).toString(); print(testOutput); - assert(testOutput == Iterable.generate(size - 1, (i) => i + 1).toString(), - testOutput); + assert( + testOutput == Iterable.generate(size - 1, (i) => i + 1).toString(), + testOutput, + ); final watch = Stopwatch(); for (var i = 1;; i++) { diff --git a/pkgs/graphs/example/crawl_async_example.dart b/pkgs/graphs/example/crawl_async_example.dart index 002b32d27c..c74eae901c 100644 --- a/pkgs/graphs/example/crawl_async_example.dart +++ b/pkgs/graphs/example/crawl_async_example.dart @@ -18,12 +18,12 @@ import 'package:pool/pool.dart'; /// asynchronously. Future main() async { // Limits calls to [findImports]. - var pool = Pool(10); - var allImports = await crawlAsync( - [Uri.parse('package:graphs/graphs.dart')], - read, - (from, source) => pool.withResource(() => findImports(from, source))) - .toList(); + final pool = Pool(10); + final allImports = await crawlAsync( + [Uri.parse('package:graphs/graphs.dart')], + read, + (from, source) => pool.withResource(() => findImports(from, source)), + ).toList(); print(allImports.map((s) => s.uri).toList()); } @@ -32,11 +32,11 @@ AnalysisContext? _analysisContext; Future get analysisContext async { var context = _analysisContext; if (context == null) { - var libUri = Uri.parse('package:graphs/'); - var libPath = await pathForUri(libUri); - var packagePath = p.dirname(libPath); + final libUri = Uri.parse('package:graphs/'); + final libPath = await pathForUri(libUri); + final packagePath = p.dirname(libPath); - var roots = ContextLocator().locateRoots(includedPaths: [packagePath]); + final roots = ContextLocator().locateRoots(includedPaths: [packagePath]); if (roots.length != 1) { throw StateError('Expected to find exactly one context root, got $roots'); } @@ -48,23 +48,22 @@ Future get analysisContext async { return context; } -Future> findImports(Uri from, Source source) async { - return source.unit.directives - .whereType() - .map((d) => d.uri.stringValue!) - .where((uri) => !uri.startsWith('dart:')) - .map((import) => resolveImport(import, from)); -} +Future> findImports(Uri from, Source source) async => + source.unit.directives + .whereType() + .map((d) => d.uri.stringValue!) + .where((uri) => !uri.startsWith('dart:')) + .map((import) => resolveImport(import, from)); Future parseUri(Uri uri) async { - var path = await pathForUri(uri); - var analysisSession = (await analysisContext).currentSession; - var parseResult = analysisSession.getParsedUnit(path); + final path = await pathForUri(uri); + final analysisSession = (await analysisContext).currentSession; + final parseResult = analysisSession.getParsedUnit(path); return (parseResult as ParsedUnitResult).unit; } Future pathForUri(Uri uri) async { - var fileUri = await Isolate.resolvePackageUri(uri); + final fileUri = await Isolate.resolvePackageUri(uri); if (fileUri == null || !fileUri.isScheme('file')) { throw StateError('Expected to resolve $uri to a file URI, got $fileUri'); } diff --git a/pkgs/graphs/example/example.dart b/pkgs/graphs/example/example.dart index 2fbb8e16f2..82cbbcabe7 100644 --- a/pkgs/graphs/example/example.dart +++ b/pkgs/graphs/example/example.dart @@ -30,18 +30,20 @@ class Node { } void main() { - var nodeA = Node('A', 1); - var nodeB = Node('B', 2); - var nodeC = Node('C', 3); - var nodeD = Node('D', 4); - var graph = Graph({ + final nodeA = Node('A', 1); + final nodeB = Node('B', 2); + final nodeC = Node('C', 3); + final nodeD = Node('D', 4); + final graph = Graph({ nodeA: [nodeB, nodeC], nodeB: [nodeC, nodeD], nodeC: [nodeB, nodeD] }); - var components = stronglyConnectedComponents( - graph.nodes.keys, (node) => graph.nodes[node] ?? []); + final components = stronglyConnectedComponents( + graph.nodes.keys, + (node) => graph.nodes[node] ?? [], + ); print(components); } diff --git a/pkgs/graphs/lib/src/crawl_async.dart b/pkgs/graphs/lib/src/crawl_async.dart index 70699ca44a..68c0a5b71d 100644 --- a/pkgs/graphs/lib/src/crawl_async.dart +++ b/pkgs/graphs/lib/src/crawl_async.dart @@ -30,9 +30,10 @@ final _empty = Future.value(); /// have not completed. If the [edges] callback needs to be limited or throttled /// that must be done by wrapping it before calling [crawlAsync]. Stream crawlAsync( - Iterable roots, - FutureOr Function(K) readNode, - FutureOr> Function(K, V) edges) { + Iterable roots, + FutureOr Function(K) readNode, + FutureOr> Function(K, V) edges, +) { final crawl = _CrawlAsync(roots, readNode, edges)..run(); return crawl.result.stream; } @@ -63,10 +64,10 @@ class _CrawlAsync { /// Resolve the node at [key] and output it, then start crawling all of it's /// edges. Future _crawlFrom(K key) async { - var value = await readNode(key); + final value = await readNode(key); if (result.isClosed) return; result.add(value); - var next = await edges(key, value); + final next = await edges(key, value); await Future.wait(next.map(_visit), eagerError: true); } diff --git a/pkgs/graphs/lib/src/shortest_path.dart b/pkgs/graphs/lib/src/shortest_path.dart index bf23620808..28f7e51aad 100644 --- a/pkgs/graphs/lib/src/shortest_path.dart +++ b/pkgs/graphs/lib/src/shortest_path.dart @@ -134,8 +134,8 @@ class _Tail extends Iterable { late final _asIterable = () { _Tail? next = this; - var reversed = List.generate(length, (_) { - var val = next!.tail; + final reversed = List.generate(length, (_) { + final val = next!.tail; next = next!.head; return val as T; }); diff --git a/pkgs/graphs/lib/src/strongly_connected_components.dart b/pkgs/graphs/lib/src/strongly_connected_components.dart index 7021ed38f3..14b1d11c10 100644 --- a/pkgs/graphs/lib/src/strongly_connected_components.dart +++ b/pkgs/graphs/lib/src/strongly_connected_components.dart @@ -43,7 +43,7 @@ List> stronglyConnectedComponents( final nonNullEquals = equals ?? _defaultEquals; var index = 0; - var lastVisited = Queue(); + final lastVisited = Queue(); void strongConnect(T node) { indexes[node] = index; diff --git a/pkgs/graphs/lib/src/topological_sort.dart b/pkgs/graphs/lib/src/topological_sort.dart index 40c09edf61..09ec2a0c17 100644 --- a/pkgs/graphs/lib/src/topological_sort.dart +++ b/pkgs/graphs/lib/src/topological_sort.dart @@ -36,19 +36,27 @@ import 'cycle_exception.dart'; /// contain no duplicate entries. /// /// Throws a [CycleException] if the graph is cyclical. -List topologicalSort(Iterable nodes, Iterable Function(T) edges, - {bool Function(T, T)? equals, - int Function(T)? hashCode, - Comparator? secondarySort}) { +List topologicalSort( + Iterable nodes, + Iterable Function(T) edges, { + bool Function(T, T)? equals, + int Function(T)? hashCode, + Comparator? secondarySort, +}) { if (secondarySort != null) { return _topologicalSortWithSecondary( - [...nodes], edges, secondarySort, equals, hashCode); + [...nodes], + edges, + secondarySort, + equals, + hashCode, + ); } // https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search - var result = QueueList(); - var permanentMark = HashSet(equals: equals, hashCode: hashCode); - var temporaryMark = LinkedHashSet(equals: equals, hashCode: hashCode); + final result = QueueList(); + final permanentMark = HashSet(equals: equals, hashCode: hashCode); + final temporaryMark = LinkedHashSet(equals: equals, hashCode: hashCode); void visit(T node) { if (permanentMark.contains(node)) return; if (temporaryMark.contains(node)) { @@ -72,18 +80,19 @@ List topologicalSort(Iterable nodes, Iterable Function(T) edges, /// An implementation of [topologicalSort] with a secondary comparison function. List _topologicalSortWithSecondary( - List nodes, - Iterable Function(T) edges, - Comparator comparator, - bool Function(T, T)? equals, - int Function(T)? hashCode) { + List nodes, + Iterable Function(T) edges, + Comparator comparator, + bool Function(T, T)? equals, + int Function(T)? hashCode, +) { // https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm, // modified to sort the nodes to traverse. Also documented in // https://www.algotree.org/algorithms/tree_graph_traversal/lexical_topological_sort_c++/ // For each node, the number of incoming edges it has that we haven't yet // traversed. - var incomingEdges = HashMap(equals: equals, hashCode: hashCode); + final incomingEdges = HashMap(equals: equals, hashCode: hashCode); for (var node in nodes) { for (var child in edges(node)) { incomingEdges[child] = (incomingEdges[child] ?? 0) + 1; @@ -91,14 +100,14 @@ List _topologicalSortWithSecondary( } // A priority queue of nodes that have no remaining incoming edges. - var nodesToTraverse = PriorityQueue(comparator); + final nodesToTraverse = PriorityQueue(comparator); for (var node in nodes) { if (!incomingEdges.containsKey(node)) nodesToTraverse.add(node); } - var result = []; + final result = []; while (nodesToTraverse.isNotEmpty) { - var node = nodesToTraverse.removeFirst(); + final node = nodesToTraverse.removeFirst(); result.add(node); for (var child in edges(node)) { var remainingEdges = incomingEdges[child]!; @@ -114,12 +123,16 @@ List _topologicalSortWithSecondary( // call the normal [topologicalSort] with a view of this graph that only // includes nodes that still have edges. bool nodeIsInCycle(T node) { - var edges = incomingEdges[node]; + final edges = incomingEdges[node]; return edges != null && edges > 0; } - topologicalSort(nodes.where(nodeIsInCycle), edges, - equals: equals, hashCode: hashCode); + topologicalSort( + nodes.where(nodeIsInCycle), + edges, + equals: equals, + hashCode: hashCode, + ); assert(false, 'topologicalSort() should throw if the graph has a cycle'); } diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index 5c3d7a011f..b935c2fb4e 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,19 +1,19 @@ name: graphs -version: 2.2.0 +version: 2.2.1-dev description: Graph algorithms that operate on graphs in any representation repository: https://github.com/dart-lang/graphs environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.18.0 <3.0.0' dependencies: collection: ^1.1.0 dev_dependencies: - lints: ^1.0.0 + lints: ^2.0.0 test: ^1.16.0 # For examples - analyzer: '>=2.0.0 <5.0.0' + analyzer: ^5.2.0 path: ^1.8.0 pool: ^1.5.0 diff --git a/pkgs/graphs/test/crawl_async_test.dart b/pkgs/graphs/test/crawl_async_test.dart index 0e9630a0a1..908b5a2ca8 100644 --- a/pkgs/graphs/test/crawl_async_test.dart +++ b/pkgs/graphs/test/crawl_async_test.dart @@ -10,23 +10,25 @@ import 'utils/graph.dart'; void main() { group('asyncCrawl', () { Future> crawl( - Map?> g, Iterable roots) { - var graph = AsyncGraph(g); + Map?> g, + Iterable roots, + ) { + final graph = AsyncGraph(g); return crawlAsync(roots, graph.readNode, graph.edges).toList(); } test('empty result for empty graph', () async { - var result = await crawl({}, []); + final result = await crawl({}, []); expect(result, isEmpty); }); test('single item for a single node', () async { - var result = await crawl({'a': []}, ['a']); + final result = await crawl({'a': []}, ['a']); expect(result, ['a']); }); test('hits every node in a graph', () async { - var result = await crawl({ + final result = await crawl({ 'a': ['b', 'c'], 'b': ['c'], 'c': ['d'], @@ -35,12 +37,14 @@ void main() { 'a' ]); expect(result, hasLength(4)); - expect(result, - allOf(contains('a'), contains('b'), contains('c'), contains('d'))); + expect( + result, + allOf(contains('a'), contains('b'), contains('c'), contains('d')), + ); }); test('handles cycles', () async { - var result = await crawl({ + final result = await crawl({ 'a': ['b'], 'b': ['c'], 'c': ['b'], @@ -52,7 +56,7 @@ void main() { }); test('handles self cycles', () async { - var result = await crawl({ + final result = await crawl({ 'a': ['b'], 'b': ['b'], }, [ @@ -63,7 +67,7 @@ void main() { }); test('allows null edges', () async { - var result = await crawl({ + final result = await crawl({ 'a': ['b'], 'b': null, }, [ @@ -74,7 +78,7 @@ void main() { }); test('allows null nodes', () async { - var result = await crawl({ + final result = await crawl({ 'a': ['b'], }, [ 'a' @@ -83,20 +87,26 @@ void main() { }); test('surfaces exceptions for crawling edges', () { - var graph = { + final graph = { 'a': ['b'], }; - var nodes = crawlAsync(['a'], (n) => n, - (k, n) => k == 'b' ? throw ArgumentError() : graph[k] ?? []); + final nodes = crawlAsync( + ['a'], + (n) => n, + (k, n) => k == 'b' ? throw ArgumentError() : graph[k] ?? [], + ); expect(nodes, emitsThrough(emitsError(isArgumentError))); }); test('surfaces exceptions for resolving keys', () { - var graph = { + final graph = { 'a': ['b'], }; - var nodes = crawlAsync(['a'], (n) => n == 'b' ? throw ArgumentError() : n, - (k, n) => graph[k] ?? []); + final nodes = crawlAsync( + ['a'], + (n) => n == 'b' ? throw ArgumentError() : n, + (k, n) => graph[k] ?? [], + ); expect(nodes, emitsThrough(emitsError(isArgumentError))); }); }); diff --git a/pkgs/graphs/test/shortest_path_test.dart b/pkgs/graphs/test/shortest_path_test.dart index 89856ab414..88281d4f6b 100644 --- a/pkgs/graphs/test/shortest_path_test.dart +++ b/pkgs/graphs/test/shortest_path_test.dart @@ -22,16 +22,20 @@ void main() { List readEdges(String key) => graph[key] ?? []; - List getXValues(X key) => - graph[key.value]?.map((v) => X(v)).toList() ?? []; + List getXValues(X key) => graph[key.value]?.map(X.new).toList() ?? []; - void _singlePathTest(String from, String to, List? expected) { + void singlePathTest(String from, String to, List? expected) { test('$from -> $to should be $expected (mapped)', () { expect( - shortestPath(X(from), X(to), getXValues, - equals: xEquals, hashCode: xHashCode) - ?.map((x) => x.value), - expected); + shortestPath( + X(from), + X(to), + getXValues, + equals: xEquals, + hashCode: xHashCode, + )?.map((x) => x.value), + expected, + ); }); test('$from -> $to should be $expected', () { @@ -39,12 +43,18 @@ void main() { }); } - void _pathsTest( - String from, Map> expected, List nullPaths) { + void pathsTest( + String from, + Map> expected, + List nullPaths, + ) { test('paths from $from (mapped)', () { - final result = shortestPaths(X(from), getXValues, - equals: xEquals, hashCode: xHashCode) - .map((k, v) => MapEntry(k.value, v.map((x) => x.value).toList())); + final result = shortestPaths( + X(from), + getXValues, + equals: xEquals, + hashCode: xHashCode, + ).map((k, v) => MapEntry(k.value, v.map((x) => x.value).toList())); expect(result, expected); }); @@ -54,15 +64,15 @@ void main() { }); for (var entry in expected.entries) { - _singlePathTest(from, entry.key, entry.value); + singlePathTest(from, entry.key, entry.value); } for (var entry in nullPaths) { - _singlePathTest(from, entry, null); + singlePathTest(from, entry, null); } } - _pathsTest('1', { + pathsTest('1', { '5': ['5'], '3': ['2', '3'], '8': ['5', '8'], @@ -74,30 +84,30 @@ void main() { '7', ]); - _pathsTest('6', { + pathsTest('6', { '7': ['7'], '6': [], }, [ '1', ]); - _pathsTest('7', {'7': []}, ['1', '6']); + pathsTest('7', {'7': []}, ['1', '6']); - _pathsTest('42', {'42': []}, ['1', '6']); + pathsTest('42', {'42': []}, ['1', '6']); test('integration test', () { // Be deterministic in the generated graph. This test may have to be updated // if the behavior of `Random` changes for the provided seed. - final _rnd = Random(1); - final size = 1000; + final rnd = Random(1); + const size = 1000; final graph = HashMap>(); Iterable? resultForGraph() => shortestPath(0, size - 1, (e) => graph[e] ?? const []); void addRandomEdge() { - final toList = graph.putIfAbsent(_rnd.nextInt(size), () => []); + final toList = graph.putIfAbsent(rnd.nextInt(size), () => []); - final toValue = _rnd.nextInt(size); + final toValue = rnd.nextInt(size); if (!toList.contains(toValue)) { toList.add(toValue); } @@ -134,10 +144,10 @@ void main() { // eventually eliminate any path. do { expect(++count, lessThan(size * 5), reason: 'This loop should finish.'); - final randomKey = graph.keys.elementAt(_rnd.nextInt(graph.length)); + final randomKey = graph.keys.elementAt(rnd.nextInt(graph.length)); final list = graph[randomKey]!; expect(list, isNotEmpty); - list.removeAt(_rnd.nextInt(list.length)); + list.removeAt(rnd.nextInt(list.length)); if (list.isEmpty) { graph.remove(randomKey); } diff --git a/pkgs/graphs/test/strongly_connected_components_test.dart b/pkgs/graphs/test/strongly_connected_components_test.dart index f147f56f8e..0c459a44f3 100644 --- a/pkgs/graphs/test/strongly_connected_components_test.dart +++ b/pkgs/graphs/test/strongly_connected_components_test.dart @@ -17,23 +17,25 @@ void main() { }) { final graph = Graph(g); return stronglyConnectedComponents( - startNodes ?? graph.allNodes, graph.edges); + startNodes ?? graph.allNodes, + graph.edges, + ); } test('empty result for empty graph', () { - var result = components({}); + final result = components({}); expect(result, isEmpty); }); test('single item for single node', () { - var result = components({'a': []}); + final result = components({'a': []}); expect(result, [ ['a'] ]); }); test('handles non-cycles', () { - var result = components({ + final result = components({ 'a': ['b'], 'b': ['c'], 'c': [] @@ -46,7 +48,7 @@ void main() { }); test('handles entire graph as cycle', () { - var result = components({ + final result = components({ 'a': ['b'], 'b': ['c'], 'c': ['a'] @@ -57,18 +59,18 @@ void main() { test('includes the first passed root last in a cycle', () { // In cases where this is used to find a topological ordering the first // value in nodes should always come last. - var graph = { + final graph = { 'a': ['b'], 'b': ['a'] }; - var resultFromA = components(graph, startNodes: ['a']); - var resultFromB = components(graph, startNodes: ['b']); + final resultFromA = components(graph, startNodes: ['a']); + final resultFromB = components(graph, startNodes: ['b']); expect(resultFromA.single.last, 'a'); expect(resultFromB.single.last, 'b'); }); test('handles cycles in the middle', () { - var result = components({ + final result = components({ 'a': ['b', 'c'], 'b': ['c', 'd'], 'c': ['b', 'd'], @@ -82,7 +84,7 @@ void main() { }); test('handles self cycles', () { - var result = components({ + final result = components({ 'a': ['b'], 'b': ['b'], }); @@ -93,7 +95,7 @@ void main() { }); test('valid topological ordering for disjoint subgraphs', () { - var result = components({ + final result = components({ 'a': ['b', 'c'], 'b': ['b1', 'b2'], 'c': ['c1', 'c2'], @@ -104,37 +106,41 @@ void main() { }); expect( - result, - containsAllInOrder([ - ['c1'], - ['c'], - ['a'] - ])); + result, + containsAllInOrder([ + ['c1'], + ['c'], + ['a'] + ]), + ); expect( - result, - containsAllInOrder([ - ['c2'], - ['c'], - ['a'] - ])); + result, + containsAllInOrder([ + ['c2'], + ['c'], + ['a'] + ]), + ); expect( - result, - containsAllInOrder([ - ['b1'], - ['b'], - ['a'] - ])); + result, + containsAllInOrder([ + ['b1'], + ['b'], + ['a'] + ]), + ); expect( - result, - containsAllInOrder([ - ['b2'], - ['b'], - ['a'] - ])); + result, + containsAllInOrder([ + ['b2'], + ['b'], + ['a'] + ]), + ); }); test('handles getting null for edges', () { - var result = components({ + final result = components({ 'a': ['b'], 'b': null, }); @@ -156,26 +162,27 @@ void main() { startNodes ??= graph.allNodes.map((n) => n.value); return stronglyConnectedComponents( - startNodes.map((n) => X(n)), graph.edges, - equals: xEquals, hashCode: xHashCode) - .map((list) => list.map((x) => x.value).toList()) - .toList(); + startNodes.map(X.new), + graph.edges, + equals: xEquals, + hashCode: xHashCode, + ).map((list) => list.map((x) => x.value).toList()).toList(); } test('empty result for empty graph', () { - var result = components({}); + final result = components({}); expect(result, isEmpty); }); test('single item for single node', () { - var result = components({'a': []}); + final result = components({'a': []}); expect(result, [ ['a'] ]); }); test('handles non-cycles', () { - var result = components({ + final result = components({ 'a': ['b'], 'b': ['c'], 'c': [] @@ -188,7 +195,7 @@ void main() { }); test('handles entire graph as cycle', () { - var result = components({ + final result = components({ 'a': ['b'], 'b': ['c'], 'c': ['a'] @@ -199,18 +206,18 @@ void main() { test('includes the first passed root last in a cycle', () { // In cases where this is used to find a topological ordering the first // value in nodes should always come last. - var graph = { + final graph = { 'a': ['b'], 'b': ['a'] }; - var resultFromA = components(graph, startNodes: ['a']); - var resultFromB = components(graph, startNodes: ['b']); + final resultFromA = components(graph, startNodes: ['a']); + final resultFromB = components(graph, startNodes: ['b']); expect(resultFromA.single.last, 'a'); expect(resultFromB.single.last, 'b'); }); test('handles cycles in the middle', () { - var result = components({ + final result = components({ 'a': ['b', 'c'], 'b': ['c', 'd'], 'c': ['b', 'd'], @@ -224,7 +231,7 @@ void main() { }); test('handles self cycles', () { - var result = components({ + final result = components({ 'a': ['b'], 'b': ['b'], }); @@ -235,7 +242,7 @@ void main() { }); test('valid topological ordering for disjoint subgraphs', () { - var result = components({ + final result = components({ 'a': ['b', 'c'], 'b': ['b1', 'b2'], 'c': ['c1', 'c2'], @@ -246,37 +253,41 @@ void main() { }); expect( - result, - containsAllInOrder([ - ['c1'], - ['c'], - ['a'] - ])); + result, + containsAllInOrder([ + ['c1'], + ['c'], + ['a'] + ]), + ); expect( - result, - containsAllInOrder([ - ['c2'], - ['c'], - ['a'] - ])); + result, + containsAllInOrder([ + ['c2'], + ['c'], + ['a'] + ]), + ); expect( - result, - containsAllInOrder([ - ['b1'], - ['b'], - ['a'] - ])); + result, + containsAllInOrder([ + ['b1'], + ['b'], + ['a'] + ]), + ); expect( - result, - containsAllInOrder([ - ['b2'], - ['b'], - ['a'] - ])); + result, + containsAllInOrder([ + ['b2'], + ['b'], + ['a'] + ]), + ); }); test('handles getting null for edges', () { - var result = components({ + final result = components({ 'a': ['b'], 'b': null, }); diff --git a/pkgs/graphs/test/topological_sort_test.dart b/pkgs/graphs/test/topological_sort_test.dart index 3870d5a1bc..8aceeaa7c4 100644 --- a/pkgs/graphs/test/topological_sort_test.dart +++ b/pkgs/graphs/test/topological_sort_test.dart @@ -21,23 +21,26 @@ void main() { }); test('with no edges', () { - expect(_topologicalSort({1: [], 2: [], 3: [], 4: []}), - unorderedEquals([1, 2, 3, 4])); + expect( + _topologicalSort({1: [], 2: [], 3: [], 4: []}), + unorderedEquals([1, 2, 3, 4]), + ); }); test('with single edges', () { expect( - _topologicalSort({ - 1: [2], - 2: [3], - 3: [4], - 4: [] - }), - equals([1, 2, 3, 4])); + _topologicalSort({ + 1: [2], + 2: [3], + 3: [4], + 4: [] + }), + equals([1, 2, 3, 4]), + ); }); test('with many edges from one node', () { - var result = _topologicalSort({ + final result = _topologicalSort({ 1: [2, 3, 4], 2: [], 3: [], @@ -49,7 +52,7 @@ void main() { }); test('with transitive edges', () { - var result = _topologicalSort({ + final result = _topologicalSort({ 1: [2, 4], 2: [], 3: [], @@ -62,7 +65,7 @@ void main() { }); test('with diamond edges', () { - var result = _topologicalSort({ + final result = _topologicalSort({ 1: [2, 3], 2: [4], 3: [4], @@ -78,40 +81,45 @@ void main() { test('respects custom equality and hash functions', () { expect( - _topologicalSort({ + _topologicalSort( + { 0: [2], 3: [4], 5: [6], 7: [] }, - equals: (i, j) => (i ~/ 2) == (j ~/ 2), - hashCode: (i) => (i ~/ 2).hashCode), - equals([ - 0, - anyOf([2, 3]), - anyOf([4, 5]), - anyOf([6, 7]) - ])); + equals: (i, j) => (i ~/ 2) == (j ~/ 2), + hashCode: (i) => (i ~/ 2).hashCode, + ), + equals([ + 0, + anyOf([2, 3]), + anyOf([4, 5]), + anyOf([6, 7]) + ]), + ); }); group('throws a CycleException for a graph with', () { test('a one-node cycle', () { expect( - () => _topologicalSort({ - 1: [1] - }), - throwsCycleException([1])); + () => _topologicalSort({ + 1: [1] + }), + throwsCycleException([1]), + ); }); test('a multi-node cycle', () { expect( - () => _topologicalSort({ - 1: [2], - 2: [3], - 3: [4], - 4: [1] - }), - throwsCycleException([1, 2, 3, 4])); + () => _topologicalSort({ + 1: [2], + 2: [3], + 3: [4], + 4: [1] + }), + throwsCycleException([1, 2, 3, 4]), + ); }); }); }); @@ -128,40 +136,51 @@ void main() { test('with no edges', () { expect( - _topologicalSort({1: [], 2: [], 3: [], 4: []}, secondarySort: true), - unorderedEquals([1, 2, 3, 4])); + _topologicalSort({1: [], 2: [], 3: [], 4: []}, secondarySort: true), + unorderedEquals([1, 2, 3, 4]), + ); }); test('with single edges', () { expect( - _topologicalSort({ + _topologicalSort( + { 1: [2], 2: [3], 3: [4], 4: [] - }, secondarySort: true), - equals([1, 2, 3, 4])); + }, + secondarySort: true, + ), + equals([1, 2, 3, 4]), + ); }); test('with many edges from one node', () { - var result = _topologicalSort({ - 1: [2, 3, 4], - 2: [], - 3: [], - 4: [] - }, secondarySort: true); + final result = _topologicalSort( + { + 1: [2, 3, 4], + 2: [], + 3: [], + 4: [] + }, + secondarySort: true, + ); expect(result.indexOf(1), lessThan(result.indexOf(2))); expect(result.indexOf(1), lessThan(result.indexOf(3))); expect(result.indexOf(1), lessThan(result.indexOf(4))); }); test('with transitive edges', () { - var result = _topologicalSort({ - 1: [2, 4], - 2: [], - 3: [], - 4: [3] - }, secondarySort: true); + final result = _topologicalSort( + { + 1: [2, 4], + 2: [], + 3: [], + 4: [3] + }, + secondarySort: true, + ); expect(result.indexOf(1), lessThan(result.indexOf(2))); expect(result.indexOf(1), lessThan(result.indexOf(3))); expect(result.indexOf(1), lessThan(result.indexOf(4))); @@ -169,12 +188,15 @@ void main() { }); test('with diamond edges', () { - var result = _topologicalSort({ - 1: [2, 3], - 2: [4], - 3: [4], - 4: [] - }, secondarySort: true); + final result = _topologicalSort( + { + 1: [2, 3], + 2: [4], + 3: [4], + 4: [] + }, + secondarySort: true, + ); expect(result.indexOf(1), lessThan(result.indexOf(2))); expect(result.indexOf(1), lessThan(result.indexOf(3))); expect(result.indexOf(1), lessThan(result.indexOf(4))); @@ -185,124 +207,156 @@ void main() { group('lexically sorts a graph where possible', () { test('with no edges', () { - var result = + final result = _topologicalSort({4: [], 3: [], 1: [], 2: []}, secondarySort: true); expect(result, equals([1, 2, 3, 4])); }); test('with one non-lexical edge', () { - var result = _topologicalSort({ - 4: [], - 3: [1], - 1: [], - 2: [] - }, secondarySort: true); + final result = _topologicalSort( + { + 4: [], + 3: [1], + 1: [], + 2: [] + }, + secondarySort: true, + ); expect( - result, - equals(anyOf([ + result, + equals( + anyOf([ [2, 3, 1, 4], [3, 1, 2, 4] - ]))); + ]), + ), + ); }); test('with a non-lexical topolgical order', () { - var result = _topologicalSort({ - 4: [3], - 3: [2], - 2: [1], - 1: [] - }, secondarySort: true); + final result = _topologicalSort( + { + 4: [3], + 3: [2], + 2: [1], + 1: [] + }, + secondarySort: true, + ); expect(result, equals([4, 3, 2, 1])); }); group('with multiple layers', () { test('in lexical order', () { - var result = _topologicalSort({ - 1: [2], - 2: [3], - 3: [], - 4: [5], - 5: [6], - 6: [] - }, secondarySort: true); + final result = _topologicalSort( + { + 1: [2], + 2: [3], + 3: [], + 4: [5], + 5: [6], + 6: [] + }, + secondarySort: true, + ); expect(result, equals([1, 2, 3, 4, 5, 6])); }); test('in non-lexical order', () { - var result = _topologicalSort({ - 1: [3], - 3: [5], - 4: [2], - 2: [6], - 5: [], - 6: [] - }, secondarySort: true); + final result = _topologicalSort( + { + 1: [3], + 3: [5], + 4: [2], + 2: [6], + 5: [], + 6: [] + }, + secondarySort: true, + ); expect( - result, - anyOf([ - equals([1, 3, 4, 2, 5, 6]), - equals([1, 4, 2, 3, 5, 6]) - ])); + result, + anyOf([ + equals([1, 3, 4, 2, 5, 6]), + equals([1, 4, 2, 3, 5, 6]) + ]), + ); }); }); }); test('respects custom equality and hash functions', () { expect( - _topologicalSort({ + _topologicalSort( + { 0: [2], 3: [4], 5: [6], 7: [] }, - equals: (i, j) => (i ~/ 2) == (j ~/ 2), - hashCode: (i) => (i ~/ 2).hashCode, - secondarySort: true), - equals([ - 0, - anyOf([2, 3]), - anyOf([4, 5]), - anyOf([6, 7]) - ])); + equals: (i, j) => (i ~/ 2) == (j ~/ 2), + hashCode: (i) => (i ~/ 2).hashCode, + secondarySort: true, + ), + equals([ + 0, + anyOf([2, 3]), + anyOf([4, 5]), + anyOf([6, 7]) + ]), + ); }); group('throws a CycleException for a graph with', () { test('a one-node cycle', () { expect( - () => _topologicalSort({ - 1: [1] - }, secondarySort: true), - throwsCycleException([1])); + () => _topologicalSort( + { + 1: [1] + }, + secondarySort: true, + ), + throwsCycleException([1]), + ); }); test('a multi-node cycle', () { expect( - () => _topologicalSort({ - 1: [2], - 2: [3], - 3: [4], - 4: [1] - }, secondarySort: true), - throwsCycleException([1, 2, 3, 4])); + () => _topologicalSort( + { + 1: [2], + 2: [3], + 3: [4], + 4: [1] + }, + secondarySort: true, + ), + throwsCycleException([1, 2, 3, 4]), + ); }); }); }); } /// Runs a topological sort on a graph represented a map from keys to edges. -List _topologicalSort(Map> graph, - {bool Function(T, T)? equals, - int Function(T)? hashCode, - bool secondarySort = false}) { +List _topologicalSort( + Map> graph, { + bool Function(T, T)? equals, + int Function(T)? hashCode, + bool secondarySort = false, +}) { if (equals != null) { graph = LinkedHashMap(equals: equals, hashCode: hashCode)..addAll(graph); } - return topologicalSort(graph.keys, (node) { - expect(graph, contains(node)); - return graph[node]!; - }, - equals: equals, - hashCode: hashCode, - secondarySort: - secondarySort ? (a, b) => (a as Comparable).compareTo(b) : null); + return topologicalSort( + graph.keys, + (node) { + expect(graph, contains(node)); + return graph[node]!; + }, + equals: equals, + hashCode: hashCode, + secondarySort: + secondarySort ? (a, b) => (a as Comparable).compareTo(b) : null, + ); } diff --git a/pkgs/graphs/test/utils/graph.dart b/pkgs/graphs/test/utils/graph.dart index 92c220e58c..ffa24b5e5f 100644 --- a/pkgs/graphs/test/utils/graph.dart +++ b/pkgs/graphs/test/utils/graph.dart @@ -22,8 +22,10 @@ class BadGraph { BadGraph(Map?> values) : _graph = LinkedHashMap(equals: xEquals, hashCode: xHashCode) - ..addEntries(values.entries.map( - (e) => MapEntry(X(e.key), e.value?.map((v) => X(v)).toList()))); + ..addEntries( + values.entries + .map((e) => MapEntry(X(e.key), e.value?.map(X.new).toList())), + ); List edges(X node) => _graph[node] ?? []; diff --git a/pkgs/graphs/test/utils/utils.dart b/pkgs/graphs/test/utils/utils.dart index 4703843f7a..447733ff56 100644 --- a/pkgs/graphs/test/utils/utils.dart +++ b/pkgs/graphs/test/utils/utils.dart @@ -11,13 +11,15 @@ int xHashCode(X a) => a.value.hashCode; /// Returns a matcher that verifies that a function throws a [CycleException] /// with the given [cycle]. -Matcher throwsCycleException(List cycle) => throwsA(allOf([ - isA>(), - predicate((exception) { - expect((exception as CycleException).cycle, equals(cycle)); - return true; - }) - ])); +Matcher throwsCycleException(List cycle) => throwsA( + allOf([ + isA>(), + predicate((exception) { + expect((exception as CycleException).cycle, equals(cycle)); + return true; + }) + ]), + ); class X { final String value; From 48e82741429160f6283d3e5d5ac9619f7f702fbe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Jan 2023 11:04:30 -0800 Subject: [PATCH 79/88] Bump actions/checkout from 3.1.0 to 3.2.0 (#78) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.1.0 to 3.2.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8...755da8c3cf115ac066823e79a1e1788f8940201b) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkgs/graphs/.github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/.github/workflows/ci.yml b/pkgs/graphs/.github/workflows/ci.yml index 272ce6e027..825d1605e5 100644 --- a/pkgs/graphs/.github/workflows/ci.yml +++ b/pkgs/graphs/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: matrix: sdk: [dev] steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d with: sdk: ${{ matrix.sdk }} @@ -42,7 +42,7 @@ jobs: os: [ubuntu-latest] sdk: [2.18.0, dev] steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d with: sdk: ${{ matrix.sdk }} From ce61894588ace53eb7ae2968c40eb1490d092cfe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Feb 2023 07:21:29 -0800 Subject: [PATCH 80/88] Bump dart-lang/setup-dart from 1.3 to 1.4 (#80) Bumps [dart-lang/setup-dart](https://github.com/dart-lang/setup-dart) from 1.3 to 1.4. - [Release notes](https://github.com/dart-lang/setup-dart/releases) - [Changelog](https://github.com/dart-lang/setup-dart/blob/main/CHANGELOG.md) - [Commits](https://github.com/dart-lang/setup-dart/compare/6a218f2413a3e78e9087f638a238f6b40893203d...a57a6c04cf7d4840e88432aad6281d1e125f0d46) --- updated-dependencies: - dependency-name: dart-lang/setup-dart dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkgs/graphs/.github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/.github/workflows/ci.yml b/pkgs/graphs/.github/workflows/ci.yml index 825d1605e5..a9d6889b5d 100644 --- a/pkgs/graphs/.github/workflows/ci.yml +++ b/pkgs/graphs/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: sdk: [dev] steps: - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - - uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d + - uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46 with: sdk: ${{ matrix.sdk }} - id: install @@ -43,7 +43,7 @@ jobs: sdk: [2.18.0, dev] steps: - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b - - uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d + - uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46 with: sdk: ${{ matrix.sdk }} - id: install From daf03a8a8f1d23b984e4f55f8c2639695df086d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Feb 2023 07:37:43 -0800 Subject: [PATCH 81/88] Bump actions/checkout from 3.2.0 to 3.3.0 (#79) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.2.0 to 3.3.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/755da8c3cf115ac066823e79a1e1788f8940201b...ac593985615ec2ede58e132d2e21d2b1cbd6127c) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkgs/graphs/.github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/.github/workflows/ci.yml b/pkgs/graphs/.github/workflows/ci.yml index a9d6889b5d..a80031d10f 100644 --- a/pkgs/graphs/.github/workflows/ci.yml +++ b/pkgs/graphs/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: matrix: sdk: [dev] steps: - - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b + - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c - uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46 with: sdk: ${{ matrix.sdk }} @@ -42,7 +42,7 @@ jobs: os: [ubuntu-latest] sdk: [2.18.0, dev] steps: - - uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b + - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c - uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46 with: sdk: ${{ matrix.sdk }} From b480ced96e126ad25297796859ff6eb1ab320afa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 15:40:18 -0700 Subject: [PATCH 82/88] Bump actions/checkout from 3.3.0 to 3.5.0 (#82) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.3.0 to 3.5.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/ac593985615ec2ede58e132d2e21d2b1cbd6127c...8f4b7f84864484a7bf31766abe9204da3cbe65b3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkgs/graphs/.github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/.github/workflows/ci.yml b/pkgs/graphs/.github/workflows/ci.yml index a80031d10f..85bb9906d6 100644 --- a/pkgs/graphs/.github/workflows/ci.yml +++ b/pkgs/graphs/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: matrix: sdk: [dev] steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 - uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46 with: sdk: ${{ matrix.sdk }} @@ -42,7 +42,7 @@ jobs: os: [ubuntu-latest] sdk: [2.18.0, dev] steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 - uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46 with: sdk: ${{ matrix.sdk }} From 536f2baff15162374984df0f75948493e3fb5b2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 15:57:25 -0700 Subject: [PATCH 83/88] Bump dart-lang/setup-dart from 1.4.0 to 1.5.0 (#81) Bumps [dart-lang/setup-dart](https://github.com/dart-lang/setup-dart) from 1.4.0 to 1.5.0. - [Release notes](https://github.com/dart-lang/setup-dart/releases) - [Changelog](https://github.com/dart-lang/setup-dart/blob/main/CHANGELOG.md) - [Commits](https://github.com/dart-lang/setup-dart/compare/a57a6c04cf7d4840e88432aad6281d1e125f0d46...d6a63dab3335f427404425de0fbfed4686d93c4f) --- updated-dependencies: - dependency-name: dart-lang/setup-dart dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkgs/graphs/.github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/.github/workflows/ci.yml b/pkgs/graphs/.github/workflows/ci.yml index 85bb9906d6..b605209590 100644 --- a/pkgs/graphs/.github/workflows/ci.yml +++ b/pkgs/graphs/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: sdk: [dev] steps: - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 - - uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46 + - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f with: sdk: ${{ matrix.sdk }} - id: install @@ -43,7 +43,7 @@ jobs: sdk: [2.18.0, dev] steps: - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 - - uses: dart-lang/setup-dart@a57a6c04cf7d4840e88432aad6281d1e125f0d46 + - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f with: sdk: ${{ matrix.sdk }} - id: install From d1641a4bcedc86815ec3be994d287d3a90d95059 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 08:47:31 -0700 Subject: [PATCH 84/88] Bump actions/checkout from 3.5.0 to 3.5.2 (#84) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.0 to 3.5.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/8f4b7f84864484a7bf31766abe9204da3cbe65b3...8e5e7e5ab8b370d6c329ec480221332ada57f0ab) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pkgs/graphs/.github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/graphs/.github/workflows/ci.yml b/pkgs/graphs/.github/workflows/ci.yml index b605209590..08737bdc9b 100644 --- a/pkgs/graphs/.github/workflows/ci.yml +++ b/pkgs/graphs/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: matrix: sdk: [dev] steps: - - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 + - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f with: sdk: ${{ matrix.sdk }} @@ -42,7 +42,7 @@ jobs: os: [ubuntu-latest] sdk: [2.18.0, dev] steps: - - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 + - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f with: sdk: ${{ matrix.sdk }} From cef9625d5ad172e0c6a56d2d744560afe67e3ca2 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 9 May 2023 16:36:37 -0700 Subject: [PATCH 85/88] Add a transitiveClosure() function (#83) This also makes topologicalSort() and stronglyConnectedComponents() iterative rather than recursive, so transitiveClosure() is able to operate on very large graphs. --- pkgs/graphs/CHANGELOG.md | 7 +- pkgs/graphs/lib/graphs.dart | 1 + .../src/strongly_connected_components.dart | 65 +++- pkgs/graphs/lib/src/topological_sort.dart | 32 +- pkgs/graphs/lib/src/transitive_closure.dart | 143 +++++++ pkgs/graphs/pubspec.yaml | 2 +- .../strongly_connected_components_test.dart | 8 +- pkgs/graphs/test/topological_sort_test.dart | 4 +- pkgs/graphs/test/transitive_closure_test.dart | 350 ++++++++++++++++++ 9 files changed, 579 insertions(+), 33 deletions(-) create mode 100644 pkgs/graphs/lib/src/transitive_closure.dart create mode 100644 pkgs/graphs/test/transitive_closure_test.dart diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index 74718101aa..ad4a6bafef 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,4 +1,9 @@ -# 2.2.1-dev +# 2.3.0 + +- Add a `transitiveClosure` function. + +- Make `stronglyConnectedComponents` and `topologicalSort` iterative rather than + recursive to avoid stack overflows on very large graphs. - Require Dart 2.18 diff --git a/pkgs/graphs/lib/graphs.dart b/pkgs/graphs/lib/graphs.dart index f272d31979..f10b2e459f 100644 --- a/pkgs/graphs/lib/graphs.dart +++ b/pkgs/graphs/lib/graphs.dart @@ -8,3 +8,4 @@ export 'src/shortest_path.dart' show shortestPath, shortestPaths; export 'src/strongly_connected_components.dart' show stronglyConnectedComponents; export 'src/topological_sort.dart' show topologicalSort; +export 'src/transitive_closure.dart' show transitiveClosure; diff --git a/pkgs/graphs/lib/src/strongly_connected_components.dart b/pkgs/graphs/lib/src/strongly_connected_components.dart index 14b1d11c10..e8a775ceb0 100644 --- a/pkgs/graphs/lib/src/strongly_connected_components.dart +++ b/pkgs/graphs/lib/src/strongly_connected_components.dart @@ -45,21 +45,45 @@ List> stronglyConnectedComponents( var index = 0; final lastVisited = Queue(); - void strongConnect(T node) { - indexes[node] = index; - var lowLink = lowLinks[node] = index; - index++; - lastVisited.addLast(node); - onStack.add(node); - for (final next in edges(node)) { + final stack = [for (final node in nodes) _StackState(node)]; + outer: + while (stack.isNotEmpty) { + final state = stack.removeLast(); + final node = state.node; + var iterator = state.iterator; + + int lowLink; + if (iterator == null) { + if (indexes.containsKey(node)) continue; + indexes[node] = index; + lowLink = lowLinks[node] = index; + index++; + iterator = edges(node).iterator; + + // Nodes with no edges are always in their own component. + if (!iterator.moveNext()) { + result.add([node]); + continue; + } + + lastVisited.addLast(node); + onStack.add(node); + } else { + lowLink = min(lowLinks[node]!, lowLinks[iterator.current]!); + } + + do { + final next = iterator.current; if (!indexes.containsKey(next)) { - strongConnect(next); - lowLink = lowLinks[node] = min(lowLink, lowLinks[next]!); + stack.add(_StackState(node, iterator)); + stack.add(_StackState(next)); + continue outer; } else if (onStack.contains(next)) { lowLink = lowLinks[node] = min(lowLink, indexes[next]!); } - } - if (lowLinks[node] == indexes[node]) { + } while (iterator.moveNext()); + + if (lowLink == indexes[node]) { final component = []; T next; do { @@ -71,10 +95,23 @@ List> stronglyConnectedComponents( } } - for (final node in nodes) { - if (!indexes.containsKey(node)) strongConnect(node); - } return result; } +/// The state of a pass on a single node in Tarjan's Algorithm. +/// +/// This is used to perform the algorithm with an explicit stack rather than +/// recursively, to avoid stack overflow errors for very large graphs. +class _StackState { + /// The node being inspected. + final T node; + + /// The iterator traversing [node]'s edges. + /// + /// This is null if the node hasn't yet begun being traversed. + final Iterator? iterator; + + _StackState(this.node, [this.iterator]); +} + bool _defaultEquals(Object a, Object b) => a == b; diff --git a/pkgs/graphs/lib/src/topological_sort.dart b/pkgs/graphs/lib/src/topological_sort.dart index 09ec2a0c17..948e6963be 100644 --- a/pkgs/graphs/lib/src/topological_sort.dart +++ b/pkgs/graphs/lib/src/topological_sort.dart @@ -57,24 +57,30 @@ List topologicalSort( final result = QueueList(); final permanentMark = HashSet(equals: equals, hashCode: hashCode); final temporaryMark = LinkedHashSet(equals: equals, hashCode: hashCode); - void visit(T node) { - if (permanentMark.contains(node)) return; + final stack = [...nodes]; + while (stack.isNotEmpty) { + final node = stack.removeLast(); + if (permanentMark.contains(node)) continue; + + // If we're visiting this node while it's already marked and not through a + // dependency, that must mean we've traversed all its dependencies and it's + // safe to add it to the result. if (temporaryMark.contains(node)) { - throw CycleException(temporaryMark); - } + temporaryMark.remove(node); + permanentMark.add(node); + result.addFirst(node); + } else { + temporaryMark.add(node); - temporaryMark.add(node); - for (var child in edges(node)) { - visit(child); + // Revisit this node once we've visited all its children. + stack.add(node); + for (var child in edges(node)) { + if (temporaryMark.contains(child)) throw CycleException(temporaryMark); + stack.add(child); + } } - temporaryMark.remove(node); - permanentMark.add(node); - result.addFirst(node); } - for (var node in nodes) { - visit(node); - } return result; } diff --git a/pkgs/graphs/lib/src/transitive_closure.dart b/pkgs/graphs/lib/src/transitive_closure.dart new file mode 100644 index 0000000000..ee19337a84 --- /dev/null +++ b/pkgs/graphs/lib/src/transitive_closure.dart @@ -0,0 +1,143 @@ +// Copyright (c) 2023, 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. + +import 'dart:collection'; + +import 'cycle_exception.dart'; +import 'strongly_connected_components.dart'; +import 'topological_sort.dart'; + +/// Returns a transitive closure of a directed graph provided by [nodes] and +/// [edges]. +/// +/// The result is a map from [nodes] to the sets of nodes that are transitively +/// reachable through [edges]. No particular ordering is guaranteed. +/// +/// If [equals] is provided, it is used to compare nodes in the graph. If +/// [equals] is omitted, the node's own [Object.==] is used instead. +/// +/// Similarly, if [hashCode] is provided, it is used to produce a hash value +/// for nodes to efficiently calculate the return value. If it is omitted, the +/// key's own [Object.hashCode] is used. +/// +/// If you supply one of [equals] or [hashCode], you should generally also to +/// supply the other. +/// +/// Note: this requires that [nodes] and each iterable returned by [edges] +/// contain no duplicate entries. +/// +/// By default, this can handle either cyclic or acyclic graphs. If [acyclic] is +/// true, this will run more efficiently but throw a [CycleException] if the +/// graph is cyclical. +Map> transitiveClosure( + Iterable nodes, + Iterable Function(T) edges, { + bool Function(T, T)? equals, + int Function(T)? hashCode, + bool acyclic = false, +}) { + if (!acyclic) { + return _cyclicTransitiveClosure( + nodes, + edges, + equals: equals, + hashCode: hashCode, + ); + } + + final topologicalOrder = + topologicalSort(nodes, edges, equals: equals, hashCode: hashCode); + final result = LinkedHashMap>(equals: equals, hashCode: hashCode); + for (final node in topologicalOrder.reversed) { + final closure = LinkedHashSet(equals: equals, hashCode: hashCode); + for (var child in edges(node)) { + closure.add(child); + closure.addAll(result[child]!); + } + + result[node] = closure; + } + + return result; +} + +/// Returns the transitive closure of a cyclic graph using [Purdom's algorithm]. +/// +/// [Purdom's algorithm]: https://algowiki-project.org/en/Purdom%27s_algorithm +/// +/// This first computes the strongly connected components of the graph and finds +/// the transitive closure of those before flattening it out into the transitive +/// closure of the entire graph. +Map> _cyclicTransitiveClosure( + Iterable nodes, + Iterable Function(T) edges, { + bool Function(T, T)? equals, + int Function(T)? hashCode, +}) { + final components = stronglyConnectedComponents( + nodes, + edges, + equals: equals, + hashCode: hashCode, + ); + final nodesToComponents = + HashMap>(equals: equals, hashCode: hashCode); + for (final component in components) { + for (final node in component) { + nodesToComponents[node] = component; + } + } + + // Because [stronglyConnectedComponents] returns the components in reverse + // topological order, we can avoid an additional topological sort here. + // Instead, we directly traverse the component list with the knowledge that + // once we reach a component, everything reachable from it has already been + // registered in [result]. + final result = LinkedHashMap>(equals: equals, hashCode: hashCode); + for (final component in components) { + final closure = LinkedHashSet(equals: equals, hashCode: hashCode); + if (_componentIncludesCycle(component, edges, equals)) { + closure.addAll(component); + } + + // De-duplicate downstream components to avoid adding the same transitive + // children over and over. + final downstreamComponents = { + for (final node in component) + for (final child in edges(node)) nodesToComponents[child]! + }; + for (final childComponent in downstreamComponents) { + if (childComponent == component) continue; + + // This if check is just for efficiency. If [childComponent] has multiple + // nodes, `result[childComponent.first]` will contain all the nodes in + // `childComponent` anyway since it's cyclical. + if (childComponent.length == 1) closure.addAll(childComponent); + closure.addAll(result[childComponent.first]!); + } + + for (final node in component) { + result[node] = closure; + } + } + return result; +} + +/// Returns whether the strongly-connected component [component] of a graph +/// defined by [edges] includes a cycle. +bool _componentIncludesCycle( + List component, + Iterable Function(T) edges, + bool Function(T, T)? equals, +) { + // A strongly-connected component with more than one node always contains a + // cycle, by definition. + if (component.length > 1) return true; + + // A component with only a single node only contains a cycle if that node has + // an edge to itself. + final node = component.single; + return edges(node) + .any((edge) => equals == null ? edge == node : equals(edge, node)); +} diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index b935c2fb4e..d201d304ba 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,5 +1,5 @@ name: graphs -version: 2.2.1-dev +version: 2.3.0 description: Graph algorithms that operate on graphs in any representation repository: https://github.com/dart-lang/graphs diff --git a/pkgs/graphs/test/strongly_connected_components_test.dart b/pkgs/graphs/test/strongly_connected_components_test.dart index 0c459a44f3..52ff4fea93 100644 --- a/pkgs/graphs/test/strongly_connected_components_test.dart +++ b/pkgs/graphs/test/strongly_connected_components_test.dart @@ -51,9 +51,13 @@ void main() { final result = components({ 'a': ['b'], 'b': ['c'], - 'c': ['a'] + 'c': ['d'], + 'd': ['a'], }); - expect(result, [allOf(contains('a'), contains('b'), contains('c'))]); + expect( + result, + [allOf(contains('a'), contains('b'), contains('c'), contains('d'))], + ); }); test('includes the first passed root last in a cycle', () { diff --git a/pkgs/graphs/test/topological_sort_test.dart b/pkgs/graphs/test/topological_sort_test.dart index 8aceeaa7c4..0cceb45cb5 100644 --- a/pkgs/graphs/test/topological_sort_test.dart +++ b/pkgs/graphs/test/topological_sort_test.dart @@ -118,7 +118,7 @@ void main() { 3: [4], 4: [1] }), - throwsCycleException([1, 2, 3, 4]), + throwsCycleException([4, 1, 2, 3]), ); }); }); @@ -331,7 +331,7 @@ void main() { }, secondarySort: true, ), - throwsCycleException([1, 2, 3, 4]), + throwsCycleException([4, 1, 2, 3]), ); }); }); diff --git a/pkgs/graphs/test/transitive_closure_test.dart b/pkgs/graphs/test/transitive_closure_test.dart new file mode 100644 index 0000000000..65ea3235ef --- /dev/null +++ b/pkgs/graphs/test/transitive_closure_test.dart @@ -0,0 +1,350 @@ +// Copyright (c) 2023, 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. + +import 'dart:collection'; + +import 'package:graphs/graphs.dart'; +import 'package:test/test.dart'; + +import 'utils/utils.dart'; + +void main() { + group('for an acyclic graph', () { + for (final acyclic in [true, false]) { + group('with acyclic: $acyclic', () { + group('returns the transitive closure for a graph', () { + test('with no nodes', () { + expect(_transitiveClosure({}, acyclic: acyclic), isEmpty); + }); + + test('with only one node', () { + expect( + _transitiveClosure({1: []}, acyclic: acyclic), + equals({1: {}}), + ); + }); + + test('with no edges', () { + expect( + _transitiveClosure( + {1: [], 2: [], 3: [], 4: []}, + acyclic: acyclic, + ), + equals(>{1: {}, 2: {}, 3: {}, 4: {}}), + ); + }); + + test('with single edges', () { + expect( + _transitiveClosure( + { + 1: [2], + 2: [3], + 3: [4], + 4: [] + }, + acyclic: acyclic, + ), + equals({ + 1: {2, 3, 4}, + 2: {3, 4}, + 3: {4}, + 4: {} + }), + ); + }); + + test('with many edges from one node', () { + expect( + _transitiveClosure( + { + 1: [2, 3, 4], + 2: [], + 3: [], + 4: [] + }, + acyclic: acyclic, + ), + equals(>{ + 1: {2, 3, 4}, + 2: {}, + 3: {}, + 4: {} + }), + ); + }); + + test('with transitive edges', () { + expect( + _transitiveClosure( + { + 1: [2, 4], + 2: [], + 3: [], + 4: [3] + }, + acyclic: acyclic, + ), + equals(>{ + 1: {2, 3, 4}, + 2: {}, + 3: {}, + 4: {3} + }), + ); + }); + + test('with diamond edges', () { + expect( + _transitiveClosure( + { + 1: [2, 3], + 2: [4], + 3: [4], + 4: [] + }, + acyclic: acyclic, + ), + equals(>{ + 1: {2, 3, 4}, + 2: {4}, + 3: {4}, + 4: {} + }), + ); + }); + + test('with disjoint subgraphs', () { + expect( + _transitiveClosure( + { + 1: [2], + 2: [3], + 3: [], + 4: [5], + 5: [6], + 6: [], + }, + acyclic: acyclic, + ), + equals(>{ + 1: {2, 3}, + 2: {3}, + 3: {}, + 4: {5, 6}, + 5: {6}, + 6: {}, + }), + ); + }); + }); + + test('respects custom equality and hash functions', () { + final result = _transitiveClosure( + { + 0: [2], + 3: [4], + 5: [6], + 7: [] + }, + equals: (i, j) => (i ~/ 2) == (j ~/ 2), + hashCode: (i) => (i ~/ 2).hashCode, + ); + + expect( + result.keys, + unorderedMatches([ + 0, + anyOf([2, 3]), + anyOf([4, 5]), + anyOf([6, 7]) + ]), + ); + expect( + result[0], + equals({ + anyOf([2, 3]), + anyOf([4, 5]), + anyOf([6, 7]) + }), + ); + expect( + result[2], + equals({ + anyOf([4, 5]), + anyOf([6, 7]) + }), + ); + expect( + result[4], + equals({ + anyOf([6, 7]) + }), + ); + expect(result[6], isEmpty); + }); + }); + } + }); + + group('for a cyclic graph', () { + group('with acyclic: true throws a CycleException for a graph with', () { + test('a one-node cycle', () { + expect( + () => _transitiveClosure( + { + 1: [1] + }, + acyclic: true, + ), + throwsCycleException([1]), + ); + }); + + test('a multi-node cycle', () { + expect( + () => _transitiveClosure( + { + 1: [2], + 2: [3], + 3: [4], + 4: [1] + }, + acyclic: true, + ), + throwsCycleException([4, 1, 2, 3]), + ); + }); + }); + + group('returns the transitive closure for a graph', () { + test('with a single one-node component', () { + expect( + _transitiveClosure({ + 1: [1] + }), + equals({ + 1: {1} + }), + ); + }); + + test('with a single multi-node component', () { + expect( + _transitiveClosure({ + 1: [2], + 2: [3], + 3: [4], + 4: [1] + }), + equals({ + 1: {1, 2, 3, 4}, + 2: {1, 2, 3, 4}, + 3: {1, 2, 3, 4}, + 4: {1, 2, 3, 4} + }), + ); + }); + + test('with a series of multi-node components', () { + expect( + _transitiveClosure({ + 1: [2], + 2: [1, 3], + 3: [4], + 4: [3, 5], + 5: [6], + 6: [5, 7], + 7: [8], + 8: [7], + }), + equals({ + 1: {1, 2, 3, 4, 5, 6, 7, 8}, + 2: {1, 2, 3, 4, 5, 6, 7, 8}, + 3: {3, 4, 5, 6, 7, 8}, + 4: {3, 4, 5, 6, 7, 8}, + 5: {5, 6, 7, 8}, + 6: {5, 6, 7, 8}, + 7: {7, 8}, + 8: {7, 8} + }), + ); + }); + + test('with a diamond of multi-node components', () { + expect( + _transitiveClosure({ + 1: [2], + 2: [1, 3, 5], + 3: [4], + 4: [3, 7], + 5: [6], + 6: [5, 7], + 7: [8], + 8: [7], + }), + equals({ + 1: {1, 2, 3, 4, 5, 6, 7, 8}, + 2: {1, 2, 3, 4, 5, 6, 7, 8}, + 3: {3, 4, 7, 8}, + 4: {3, 4, 7, 8}, + 5: {5, 6, 7, 8}, + 6: {5, 6, 7, 8}, + 7: {7, 8}, + 8: {7, 8} + }), + ); + }); + + test('mixed single- and multi-node components', () { + expect( + _transitiveClosure({ + 1: [2], + 2: [1, 3], + 3: [4], + 4: [5], + 5: [4, 6], + 6: [7], + 7: [8], + 8: [7], + }), + equals({ + 1: {1, 2, 3, 4, 5, 6, 7, 8}, + 2: {1, 2, 3, 4, 5, 6, 7, 8}, + 3: {4, 5, 6, 7, 8}, + 4: {4, 5, 6, 7, 8}, + 5: {4, 5, 6, 7, 8}, + 6: {7, 8}, + 7: {7, 8}, + 8: {7, 8} + }), + ); + }); + }); + }); +} + +/// Returns the transitive closure of a graph represented a map from keys to +/// edges. +Map> _transitiveClosure( + Map> graph, { + bool Function(T, T)? equals, + int Function(T)? hashCode, + bool acyclic = false, +}) { + assert((equals == null) == (hashCode == null)); + if (equals != null) { + graph = LinkedHashMap(equals: equals, hashCode: hashCode)..addAll(graph); + } + return transitiveClosure( + graph.keys, + (node) { + expect(graph, contains(node)); + return graph[node]!; + }, + equals: equals, + hashCode: hashCode, + acyclic: acyclic, + ); +} From d3bea488c8d1e3320fa5481aad4999195e0e9964 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Wed, 10 May 2023 09:15:52 -0700 Subject: [PATCH 86/88] Normalize changelog formatting (#85) Use `-` over `*` and avoid extra line breaks for consistency. --- pkgs/graphs/CHANGELOG.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index ad4a6bafef..e41868da09 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,21 +1,19 @@ # 2.3.0 - Add a `transitiveClosure` function. - - Make `stronglyConnectedComponents` and `topologicalSort` iterative rather than recursive to avoid stack overflows on very large graphs. - - Require Dart 2.18 # 2.2.0 -* Add a `secondarySort` parameter to the `topologicalSort()` function which +- Add a `secondarySort` parameter to the `topologicalSort()` function which applies an additional lexical sort where that doesn't break the topological sort. # 2.1.0 -* Add a `topologicalSort()` function. +- Add a `topologicalSort()` function. # 2.0.0 From 42218af08eb2d6289bb2caa4b55860625233bf3f Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Tue, 16 May 2023 12:33:43 -0700 Subject: [PATCH 87/88] blast_repo fixes (#87) dependabot --- pkgs/graphs/.github/dependabot.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkgs/graphs/.github/dependabot.yaml b/pkgs/graphs/.github/dependabot.yaml index 214481934b..439e796b48 100644 --- a/pkgs/graphs/.github/dependabot.yaml +++ b/pkgs/graphs/.github/dependabot.yaml @@ -2,7 +2,9 @@ version: 2 updates: - - package-ecosystem: "github-actions" - directory: "/" + - package-ecosystem: github-actions + directory: / schedule: - interval: "monthly" + interval: monthly + labels: + - autosubmit From 6b2f660672aab910f384f8eb48f2c4633528accc Mon Sep 17 00:00:00 2001 From: Daco Harkes Date: Tue, 23 May 2023 09:54:19 +0200 Subject: [PATCH 88/88] Prepare to merge into the tools repository Commits have been rewritten with git-filter-repo to be inside pkgs/graphs/. Tags have been rewritten with git-filter-repo to prefix graph-. Migrated GitHub action workflow. Updated `repository` config and README to use the anticipated URL. Updated CHANGELOG to have ## headings. --- .../ci.yml => .github/workflows/graphs.yml | 16 +++++++++++-- pkgs/graphs/.github/dependabot.yaml | 10 -------- pkgs/graphs/CHANGELOG.md | 24 +++++++++---------- pkgs/graphs/README.md | 2 +- pkgs/graphs/pubspec.yaml | 2 +- 5 files changed, 28 insertions(+), 26 deletions(-) rename pkgs/graphs/.github/workflows/ci.yml => .github/workflows/graphs.yml (81%) delete mode 100644 pkgs/graphs/.github/dependabot.yaml diff --git a/pkgs/graphs/.github/workflows/ci.yml b/.github/workflows/graphs.yml similarity index 81% rename from pkgs/graphs/.github/workflows/ci.yml rename to .github/workflows/graphs.yml index 08737bdc9b..3070eb11f4 100644 --- a/pkgs/graphs/.github/workflows/ci.yml +++ b/.github/workflows/graphs.yml @@ -3,9 +3,15 @@ name: CI on: # Run on PRs and pushes to the default branch. push: - branches: [ master ] + branches: [ main ] + paths: + - '.github/workflows/graphs.yml' + - 'pkgs/graphs/**' pull_request: - branches: [ master ] + branches: [ main ] + paths: + - '.github/workflows/graphs.yml' + - 'pkgs/graphs/**' schedule: - cron: "0 0 * * 0" @@ -17,6 +23,9 @@ jobs: # against Dart beta. analyze: runs-on: ubuntu-latest + defaults: + run: + working-directory: pkgs/graphs/ strategy: fail-fast: false matrix: @@ -36,6 +45,9 @@ jobs: test: needs: analyze runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: pkgs/graphs/ strategy: fail-fast: false matrix: diff --git a/pkgs/graphs/.github/dependabot.yaml b/pkgs/graphs/.github/dependabot.yaml deleted file mode 100644 index 439e796b48..0000000000 --- a/pkgs/graphs/.github/dependabot.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# Dependabot configuration file. -version: 2 - -updates: - - package-ecosystem: github-actions - directory: / - schedule: - interval: monthly - labels: - - autosubmit diff --git a/pkgs/graphs/CHANGELOG.md b/pkgs/graphs/CHANGELOG.md index e41868da09..d29ca79ce7 100644 --- a/pkgs/graphs/CHANGELOG.md +++ b/pkgs/graphs/CHANGELOG.md @@ -1,32 +1,32 @@ -# 2.3.0 +## 2.3.0 - Add a `transitiveClosure` function. - Make `stronglyConnectedComponents` and `topologicalSort` iterative rather than recursive to avoid stack overflows on very large graphs. - Require Dart 2.18 -# 2.2.0 +## 2.2.0 - Add a `secondarySort` parameter to the `topologicalSort()` function which applies an additional lexical sort where that doesn't break the topological sort. -# 2.1.0 +## 2.1.0 - Add a `topologicalSort()` function. -# 2.0.0 +## 2.0.0 - **Breaking**: `crawlAsync` will no longer ignore a node from the graph if the `readNode` callback returns null. -# 1.0.0 +## 1.0.0 - Migrate to null safety. - **Breaking**: Paths from `shortestPath[s]` are now returned as iterables to reduce memory consumption of the algorithm to O(n). -# 0.2.0 +## 0.2.0 - **BREAKING** `shortestPath`, `shortestPaths` and `stronglyConnectedComponents` now have one generic parameter and have replaced the `key` parameter with @@ -35,30 +35,30 @@ `LinkedHashMap`. It improves the usability and performance of the case where the source values are directly usable in a hash data structure. -# 0.1.3+1 +## 0.1.3+1 - Fixed a bug with non-identity `key` in `shortestPath` and `shortestPaths`. -# 0.1.3 +## 0.1.3 - Added `shortestPath` and `shortestPaths` functions. - Use `HashMap` and `HashSet` from `dart:collection` for `stronglyConnectedComponents`. Improves runtime performance. -# 0.1.2+1 +## 0.1.2+1 - Allow using non-dev Dart 2 SDK. -# 0.1.2 +## 0.1.2 - `crawlAsync` surfaces exceptions while crawling through the result stream rather than as uncaught asynchronous errors. -# 0.1.1 +## 0.1.1 - `crawlAsync` will now ignore nodes that are resolved to `null`. -# 0.1.0 +## 0.1.0 - Initial release with an implementation of `stronglyConnectedComponents` and `crawlAsync`. diff --git a/pkgs/graphs/README.md b/pkgs/graphs/README.md index a47ca8d2e0..2bf0eec6a5 100644 --- a/pkgs/graphs/README.md +++ b/pkgs/graphs/README.md @@ -1,4 +1,4 @@ -[![CI](https://github.com/dart-lang/graphs/actions/workflows/ci.yml/badge.svg)](https://github.com/dart-lang/graphs/actions/workflows/ci.yml) +[![CI](https://github.com/dart-lang/tools/actions/workflows/graphs.yml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/graphs.yml) [![pub package](https://img.shields.io/pub/v/graphs.svg)](https://pub.dev/packages/graphs) [![package publisher](https://img.shields.io/pub/publisher/graphs.svg)](https://pub.dev/packages/graphs/publisher) diff --git a/pkgs/graphs/pubspec.yaml b/pkgs/graphs/pubspec.yaml index d201d304ba..d3d9e771de 100644 --- a/pkgs/graphs/pubspec.yaml +++ b/pkgs/graphs/pubspec.yaml @@ -1,7 +1,7 @@ name: graphs version: 2.3.0 description: Graph algorithms that operate on graphs in any representation -repository: https://github.com/dart-lang/graphs +repository: https://github.com/dart-lang/tools/tree/main/pkgs/graphs environment: sdk: '>=2.18.0 <3.0.0'