[kernel] Add tool for checking AST equivalence

Change-Id: Ie06776203080e91346582534af2d56c24581bd54
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/413200
Reviewed-by: Jens Johansen <jensj@google.com>
Commit-Queue: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/front_end/tool/generate_ast_equivalence.dart b/pkg/front_end/tool/generate_ast_equivalence.dart
index c401b0f..56b049c 100644
--- a/pkg/front_end/tool/generate_ast_equivalence.dart
+++ b/pkg/front_end/tool/generate_ast_equivalence.dart
@@ -150,8 +150,6 @@
         registerAstClassEquivalence(utilityFieldType.astClass);
         sb.writeln('''($thisName, $otherName, _) {
     if (identical($thisName, $otherName)) return true;
-    if ($thisName is! ${utilityFieldType.astClass.name}) return false;
-    if ($otherName is! ${utilityFieldType.astClass.name}) return false;
     return ${classCheckName(utilityFieldType.astClass)}(
         visitor,
         $thisName,
@@ -209,8 +207,6 @@
         registerAstClassEquivalence(utilityFieldType.astClass);
         sb.writeln('''($thisName, $otherName, _) {
     if (identical($thisName, $otherName)) return true;
-    if ($thisName is! ${utilityFieldType.astClass.name}) return false;
-    if ($otherName is! ${utilityFieldType.astClass.name}) return false;
     return ${classCheckName(utilityFieldType.astClass)}(
         visitor,
         $thisName,
@@ -625,7 +621,7 @@
   bool $checkLists<E>(
             List<E>? a,
             List<E>? b,
-            bool Function(E?, E?, String) equivalentValues,
+            bool Function(E, E, String) equivalentValues,
             [String propertyName = '']) {
           if (identical(a, b)) return true;
           if (a == null || b == null) return false;
@@ -651,8 +647,8 @@
   bool $checkSets<E>(
             Set<E>? a,
             Set<E>? b,
-            bool Function(E?, E?) matchingValues,
-            bool Function(E?, E?, String) equivalentValues,
+            bool Function(E, E) matchingValues,
+            bool Function(E, E, String) equivalentValues,
             [String propertyName = '']) {
           if (identical(a, b)) return true;
           if (a == null || b == null) return false;
@@ -702,9 +698,9 @@
   bool $checkMaps<K, V>(
             Map<K, V>? a,
             Map<K, V>? b,
-            bool Function(K?, K?) matchingKeys,
-            bool Function(K?, K?, String) equivalentKeys,
-            bool Function(V?, V?, String) equivalentValues,
+            bool Function(K, K) matchingKeys,
+            bool Function(K, K, String) equivalentKeys,
+            bool Function(V, V, String) equivalentValues,
             [String propertyName = '']) {
           if (identical(a, b)) return true;
           if (a == null || b == null) return false;
@@ -733,7 +729,7 @@
             }
             if (hasFoundKey) {
               bKeys.remove(foundKey);
-              if (!equivalentValues(a[aKey], b[foundKey],
+              if (!equivalentValues(a[aKey]!, b[foundKey]!,
                   '\${propertyName}[\${aKey}]')) {
                 return false;
               }
diff --git a/pkg/kernel/bin/check_equivalence.dart b/pkg/kernel/bin/check_equivalence.dart
new file mode 100644
index 0000000..a9cbf2e
--- /dev/null
+++ b/pkg/kernel/bin/check_equivalence.dart
@@ -0,0 +1,6 @@
+#!/usr/bin/env dart
+// Copyright (c) 2025, 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 'package:kernel/src/tool/check_equivalence.dart';
diff --git a/pkg/kernel/lib/src/equivalence.dart b/pkg/kernel/lib/src/equivalence.dart
index fc463b9..0c3bb47 100644
--- a/pkg/kernel/lib/src/equivalence.dart
+++ b/pkg/kernel/lib/src/equivalence.dart
@@ -1253,7 +1253,7 @@
   /// If run in a checking state, the [propertyName] is used for registering
   /// inequivalences.
   bool checkLists<E>(
-      List<E>? a, List<E>? b, bool Function(E?, E?, String) equivalentValues,
+      List<E>? a, List<E>? b, bool Function(E, E, String) equivalentValues,
       [String propertyName = '']) {
     if (identical(a, b)) return true;
     if (a == null || b == null) return false;
@@ -1276,8 +1276,8 @@
   ///
   /// If run in a checking state, the [propertyName] is used for registering
   /// inequivalences.
-  bool checkSets<E>(Set<E>? a, Set<E>? b, bool Function(E?, E?) matchingValues,
-      bool Function(E?, E?, String) equivalentValues,
+  bool checkSets<E>(Set<E>? a, Set<E>? b, bool Function(E, E) matchingValues,
+      bool Function(E, E, String) equivalentValues,
       [String propertyName = '']) {
     if (identical(a, b)) return true;
     if (a == null || b == null) return false;
@@ -1325,9 +1325,9 @@
   bool checkMaps<K, V>(
       Map<K, V>? a,
       Map<K, V>? b,
-      bool Function(K?, K?) matchingKeys,
-      bool Function(K?, K?, String) equivalentKeys,
-      bool Function(V?, V?, String) equivalentValues,
+      bool Function(K, K) matchingKeys,
+      bool Function(K, K, String) equivalentKeys,
+      bool Function(V, V, String) equivalentValues,
       [String propertyName = '']) {
     if (identical(a, b)) return true;
     if (a == null || b == null) return false;
@@ -1355,7 +1355,7 @@
       if (hasFoundKey) {
         bKeys.remove(foundKey);
         if (!equivalentValues(
-            a[aKey], b[foundKey], '${propertyName}[${aKey}]')) {
+            a[aKey]!, b[foundKey]!, '${propertyName}[${aKey}]')) {
           return false;
         }
       } else {
@@ -5716,8 +5716,6 @@
       EquivalenceVisitor visitor, MapConstant node, MapConstant other) {
     return visitor.checkLists(node.entries, other.entries, (a, b, _) {
       if (identical(a, b)) return true;
-      if (a is! ConstantMapEntry) return false;
-      if (b is! ConstantMapEntry) return false;
       return checkConstantMapEntry(visitor, a, b);
     }, 'entries');
   }
@@ -5902,8 +5900,6 @@
     return visitor.checkMaps(node.uriToSource, other.uriToSource,
         visitor.matchValues, visitor.checkValues, (a, b, _) {
       if (identical(a, b)) return true;
-      if (a is! Source) return false;
-      if (b is! Source) return false;
       return checkSource(visitor, a, b);
     }, 'uriToSource');
   }
@@ -5914,8 +5910,6 @@
         node.metadata, other.metadata, visitor.matchValues, visitor.checkValues,
         (a, b, _) {
       if (identical(a, b)) return true;
-      if (a is! MetadataRepository) return false;
-      if (b is! MetadataRepository) return false;
       return checkMetadataRepository(visitor, a, b);
     }, 'metadata');
   }
@@ -6089,8 +6083,6 @@
     return visitor.checkLists(node.memberDescriptors, other.memberDescriptors,
         (a, b, _) {
       if (identical(a, b)) return true;
-      if (a is! ExtensionMemberDescriptor) return false;
-      if (b is! ExtensionMemberDescriptor) return false;
       return checkExtensionMemberDescriptor(visitor, a, b);
     }, 'memberDescriptors');
   }
@@ -6192,8 +6184,6 @@
     return visitor.checkLists(node.memberDescriptors, other.memberDescriptors,
         (a, b, _) {
       if (identical(a, b)) return true;
-      if (a is! ExtensionTypeMemberDescriptor) return false;
-      if (b is! ExtensionTypeMemberDescriptor) return false;
       return checkExtensionTypeMemberDescriptor(visitor, a, b);
     }, 'memberDescriptors');
   }
diff --git a/pkg/kernel/lib/src/tool/check_equivalence.dart b/pkg/kernel/lib/src/tool/check_equivalence.dart
new file mode 100644
index 0000000..42dea47
--- /dev/null
+++ b/pkg/kernel/lib/src/tool/check_equivalence.dart
@@ -0,0 +1,408 @@
+// Copyright (c) 2025, 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:io';
+
+import 'package:_fe_analyzer_shared/src/util/options.dart';
+import 'package:kernel/ast.dart';
+import 'package:kernel/src/equivalence.dart';
+import 'package:kernel/src/tool/command_line_util.dart';
+
+void main(List<String> args) {
+  ParsedOptions parsedOptions = ParsedOptions.parse(args, optionSpecification);
+
+  CommandLineHelper.requireVariableArgumentCount([2], parsedOptions.arguments,
+      () {
+    print('''
+Usage:
+
+    dart check_equivalence.dart [options] <dill1> <dill2>
+''');
+  });
+  if (parsedOptions.arguments.length != 2) {
+    exit(1);
+  }
+
+  bool unordered = Options.unordered.read(parsedOptions);
+  bool unorderedLibraries =
+      unordered || Options.unorderedLibraries.read(parsedOptions);
+  bool unorderedLibraryDependencies =
+      unordered || Options.unorderedLibraries.read(parsedOptions);
+  bool unorderedAdditionalExports =
+      unordered || Options.unorderedLibraries.read(parsedOptions);
+  bool unorderedParts =
+      unordered || Options.unorderedLibraries.read(parsedOptions);
+  bool unorderedTypedefs =
+      unordered || Options.unorderedLibraries.read(parsedOptions);
+  bool unorderedClasses =
+      unordered || Options.unorderedLibraries.read(parsedOptions);
+  bool unorderedMembers =
+      unordered || Options.unorderedMembers.read(parsedOptions);
+  bool unorderedFields =
+      unorderedMembers || Options.unorderedFields.read(parsedOptions);
+  bool unorderedProcedures =
+      unorderedMembers || Options.unorderedProcedures.read(parsedOptions);
+  bool unorderedConstructors =
+      unorderedMembers || Options.unorderedConstructors.read(parsedOptions);
+  bool unorderedAnnotations =
+      unordered || Options.unorderedAnnotations.read(parsedOptions);
+
+  Component dill1 = CommandLineHelper.tryLoadDill(parsedOptions.arguments[0]);
+  Component dill2 = CommandLineHelper.tryLoadDill(parsedOptions.arguments[1]);
+  EquivalenceResult result = checkEquivalence(dill1, dill2,
+      strategy: new Strategy(
+          unorderedLibraries: unorderedLibraries,
+          unorderedLibraryDependencies: unorderedLibraryDependencies,
+          unorderedAdditionalExports: unorderedAdditionalExports,
+          unorderedParts: unorderedParts,
+          unorderedTypedefs: unorderedTypedefs,
+          unorderedClasses: unorderedClasses,
+          unorderedFields: unorderedFields,
+          unorderedProcedures: unorderedProcedures,
+          unorderedConstructors: unorderedConstructors,
+          unorderedAnnotations: unorderedAnnotations));
+  if (result.isEquivalent) {
+    print('The dills are equivalent.');
+  } else {
+    print('Inequivalence found:');
+    print(result);
+  }
+}
+
+EquivalenceResult checkNodeEquivalence(
+  Node node1,
+  Node node2, {
+  bool unorderedLibraries = false,
+  bool unorderedLibraryDependencies = false,
+  bool unorderedAdditionalExports = false,
+  bool unorderedParts = false,
+  bool unorderedTypedefs = false,
+  bool unorderedClasses = false,
+  bool unorderedFields = false,
+  bool unorderedProcedures = false,
+  bool unorderedConstructors = false,
+  bool unorderedAnnotations = false,
+}) {
+  return checkEquivalence(node1, node2,
+      strategy: new Strategy(
+          unorderedLibraries: unorderedLibraries,
+          unorderedLibraryDependencies: unorderedLibraryDependencies,
+          unorderedAdditionalExports: unorderedAdditionalExports,
+          unorderedParts: unorderedParts,
+          unorderedTypedefs: unorderedTypedefs,
+          unorderedClasses: unorderedClasses,
+          unorderedFields: unorderedFields,
+          unorderedProcedures: unorderedProcedures,
+          unorderedConstructors: unorderedConstructors,
+          unorderedAnnotations: unorderedAnnotations));
+}
+
+class Strategy extends EquivalenceStrategy {
+  final bool unorderedLibraries;
+  final bool unorderedLibraryDependencies;
+  final bool unorderedAdditionalExports;
+  final bool unorderedParts;
+  final bool unorderedTypedefs;
+  final bool unorderedClasses;
+  final bool unorderedFields;
+  final bool unorderedProcedures;
+  final bool unorderedConstructors;
+  final bool unorderedAnnotations;
+
+  Strategy(
+      {required this.unorderedLibraries,
+      required this.unorderedLibraryDependencies,
+      required this.unorderedAdditionalExports,
+      required this.unorderedParts,
+      required this.unorderedTypedefs,
+      required this.unorderedClasses,
+      required this.unorderedFields,
+      required this.unorderedProcedures,
+      required this.unorderedConstructors,
+      required this.unorderedAnnotations});
+
+  @override
+  bool checkComponent_libraries(
+      EquivalenceVisitor visitor, Component node, Component other) {
+    if (unorderedLibraries) {
+      return visitor.checkSets(node.libraries.toSet(), other.libraries.toSet(),
+          visitor.matchNamedNodes, visitor.checkNodes, 'libraries');
+    } else {
+      return visitor.checkLists(
+          node.libraries, other.libraries, visitor.checkNodes, 'libraries');
+    }
+  }
+
+  @override
+  bool checkLibrary_dependencies(
+      EquivalenceVisitor visitor, Library node, Library other) {
+    if (unorderedLibraryDependencies) {
+      return visitor
+          .checkSets(node.dependencies.toSet(), other.dependencies.toSet(),
+              (LibraryDependency dependency1, LibraryDependency dependency2) {
+        return visitor.matchReferences(dependency1.importedLibraryReference,
+                dependency2.importedLibraryReference) &&
+            dependency1.flags == dependency2.flags &&
+            dependency1.name == dependency2.name &&
+            dependency1.fileOffset == dependency2.fileOffset;
+      }, visitor.checkNodes, 'dependencies');
+    } else {
+      return visitor.checkLists(node.dependencies, other.dependencies,
+          visitor.checkNodes, 'dependencies');
+    }
+  }
+
+  @override
+  bool checkLibrary_parts(
+      EquivalenceVisitor visitor, Library node, Library other) {
+    if (unorderedParts) {
+      return visitor.checkSets(node.parts.toSet(), other.parts.toSet(),
+          (LibraryPart part1, LibraryPart part2) {
+        return part1.partUri == part2.partUri;
+      }, visitor.checkNodes, 'parts');
+    } else {
+      return visitor.checkLists(
+          node.parts, other.parts, visitor.checkNodes, 'parts');
+    }
+  }
+
+  @override
+  bool checkLibrary_typedefs(
+      EquivalenceVisitor visitor, Library node, Library other) {
+    if (unorderedTypedefs) {
+      return visitor.checkSets(node.typedefs.toSet(), other.typedefs.toSet(),
+          visitor.matchNamedNodes, visitor.checkNodes, 'typedefs');
+    } else {
+      return visitor.checkLists(
+          node.typedefs, other.typedefs, visitor.checkNodes, 'typedefs');
+    }
+  }
+
+  @override
+  bool checkLibrary_additionalExports(
+      EquivalenceVisitor visitor, Library node, Library other) {
+    if (unorderedAdditionalExports) {
+      return visitor.checkSets(
+          node.additionalExports.toSet(),
+          other.additionalExports.toSet(),
+          visitor.matchReferences,
+          visitor.checkReferences,
+          'additionalExports');
+    } else {
+      return visitor.checkLists(node.additionalExports, other.additionalExports,
+          visitor.checkReferences, 'additionalExports');
+    }
+  }
+
+  @override
+  bool checkLibrary_classes(
+      EquivalenceVisitor visitor, Library node, Library other) {
+    if (unorderedClasses) {
+      return visitor.checkSets(node.classes.toSet(), other.classes.toSet(),
+          visitor.matchNamedNodes, visitor.checkNodes, 'classes');
+    } else {
+      return visitor.checkLists(
+          node.classes, other.classes, visitor.checkNodes, 'classes');
+    }
+  }
+
+  @override
+  bool checkLibrary_fields(
+      EquivalenceVisitor visitor, Library node, Library other) {
+    if (unorderedFields) {
+      return visitor.checkSets(node.fields.toSet(), other.fields.toSet(),
+          visitor.matchNamedNodes, visitor.checkNodes, 'fields');
+    } else {
+      return visitor.checkLists(
+          node.fields, other.fields, visitor.checkNodes, 'fields');
+    }
+  }
+
+  @override
+  bool checkLibrary_procedures(
+      EquivalenceVisitor visitor, Library node, Library other) {
+    if (unorderedProcedures) {
+      return visitor.checkSets(
+          node.procedures.toSet(),
+          other.procedures.toSet(),
+          visitor.matchNamedNodes,
+          visitor.checkNodes,
+          'procedures');
+    } else {
+      return visitor.checkLists(
+          node.procedures, other.procedures, visitor.checkNodes, 'procedures');
+    }
+  }
+
+  @override
+  bool checkLibrary_annotations(
+      EquivalenceVisitor visitor, Library node, Library other) {
+    if (unorderedAnnotations) {
+      return visitor.checkSets(
+          node.annotations.toSet(),
+          other.annotations.toSet(),
+          _matchAnnotations,
+          visitor.checkNodes,
+          'annotations');
+    } else {
+      return visitor.checkLists(node.annotations, other.annotations,
+          visitor.checkNodes, 'annotations');
+    }
+  }
+
+  @override
+  bool checkClass_fields(EquivalenceVisitor visitor, Class node, Class other) {
+    if (unorderedFields) {
+      return visitor.checkSets(node.fields.toSet(), other.fields.toSet(),
+          visitor.matchNamedNodes, visitor.checkNodes, 'fields');
+    } else {
+      return visitor.checkLists(
+          node.fields, other.fields, visitor.checkNodes, 'fields');
+    }
+  }
+
+  @override
+  bool checkClass_procedures(
+      EquivalenceVisitor visitor, Class node, Class other) {
+    if (unorderedProcedures) {
+      return visitor.checkSets(
+          node.procedures.toSet(),
+          other.procedures.toSet(),
+          visitor.matchNamedNodes,
+          visitor.checkNodes,
+          'procedures');
+    } else {
+      return visitor.checkLists(
+          node.procedures, other.procedures, visitor.checkNodes, 'procedures');
+    }
+  }
+
+  @override
+  bool checkClass_constructors(
+      EquivalenceVisitor visitor, Class node, Class other) {
+    if (unorderedConstructors) {
+      return visitor.checkSets(
+          node.constructors.toSet(),
+          other.constructors.toSet(),
+          visitor.matchNamedNodes,
+          visitor.checkNodes,
+          'constructors');
+    } else {
+      return visitor.checkLists(node.constructors, other.constructors,
+          visitor.checkNodes, 'constructors');
+    }
+  }
+
+  @override
+  bool checkClass_annotations(
+      EquivalenceVisitor visitor, Class node, Class other) {
+    if (unorderedAnnotations) {
+      return visitor.checkSets(
+          node.annotations.toSet(),
+          other.annotations.toSet(),
+          _matchAnnotations,
+          visitor.checkNodes,
+          'annotations');
+    } else {
+      return visitor.checkLists(node.annotations, other.annotations,
+          visitor.checkNodes, 'annotations');
+    }
+  }
+
+  @override
+  bool checkExtension_annotations(
+      EquivalenceVisitor visitor, Extension node, Extension other) {
+    if (unorderedAnnotations) {
+      return visitor.checkSets(
+          node.annotations.toSet(),
+          other.annotations.toSet(),
+          _matchAnnotations,
+          visitor.checkNodes,
+          'annotations');
+    } else {
+      return visitor.checkLists(node.annotations, other.annotations,
+          visitor.checkNodes, 'annotations');
+    }
+  }
+
+  @override
+  bool checkMember_annotations(
+      EquivalenceVisitor visitor, Member node, Member other) {
+    if (unorderedAnnotations) {
+      return visitor.checkSets(
+          node.annotations.toSet(),
+          other.annotations.toSet(),
+          _matchAnnotations,
+          visitor.checkNodes,
+          'annotations');
+    } else {
+      return visitor.checkLists(node.annotations, other.annotations,
+          visitor.checkNodes, 'annotations');
+    }
+  }
+
+  bool _matchAnnotations(Expression expression1, Expression expression2) {
+    return expression1.runtimeType == expression2.runtimeType &&
+        expression1.fileOffset == expression2.fileOffset;
+  }
+}
+
+class Flags {
+  static const String unordered = '--unordered';
+  static const String unorderedLibraries = '--unordered-libraries';
+  static const String unorderedLibraryDependencies =
+      '--unordered-library-dependencies';
+  static const String unorderedParts = '--unordered-parts';
+  static const String unorderedAdditionalExports =
+      '--unordered-additional-exports';
+  static const String unorderedTypedefs = '--unordered-typedefs';
+  static const String unorderedClasses = '--unordered-classes';
+  static const String unorderedMembers = '--unordered-members';
+  static const String unorderedFields = '--unordered-fields';
+  static const String unorderedProcedures = '--unordered-procedures';
+  static const String unorderedConstructors = '--unordered-constructors';
+  static const String unorderedAnnotations = '--unordered-annotations';
+}
+
+class Options {
+  static const Option<bool> unordered =
+      const Option(Flags.unordered, const BoolValue(false));
+  static const Option<bool> unorderedLibraries =
+      const Option(Flags.unorderedLibraries, const BoolValue(false));
+  static const Option<bool> unorderedLibraryDependencies =
+      const Option(Flags.unorderedLibraryDependencies, const BoolValue(false));
+  static const Option<bool> unorderedParts =
+      const Option(Flags.unorderedParts, const BoolValue(false));
+  static const Option<bool> unorderedAdditionalExports =
+      const Option(Flags.unorderedAdditionalExports, const BoolValue(false));
+  static const Option<bool> unorderedTypedefs =
+      const Option(Flags.unorderedTypedefs, const BoolValue(false));
+  static const Option<bool> unorderedClasses =
+      const Option(Flags.unorderedClasses, const BoolValue(false));
+  static const Option<bool> unorderedMembers =
+      const Option(Flags.unorderedMembers, const BoolValue(false));
+  static const Option<bool> unorderedFields =
+      const Option(Flags.unorderedFields, const BoolValue(false));
+  static const Option<bool> unorderedProcedures =
+      const Option(Flags.unorderedProcedures, const BoolValue(false));
+  static const Option<bool> unorderedConstructors =
+      const Option(Flags.unorderedConstructors, const BoolValue(false));
+  static const Option<bool> unorderedAnnotations =
+      const Option(Flags.unorderedAnnotations, const BoolValue(false));
+}
+
+const List<Option> optionSpecification = [
+  Options.unordered,
+  Options.unorderedLibraries,
+  Options.unorderedLibraryDependencies,
+  Options.unorderedParts,
+  Options.unorderedAdditionalExports,
+  Options.unorderedTypedefs,
+  Options.unorderedClasses,
+  Options.unorderedMembers,
+  Options.unorderedFields,
+  Options.unorderedProcedures,
+  Options.unorderedConstructors,
+  Options.unorderedAnnotations,
+];
diff --git a/pkg/kernel/test/check_equivalence_test.dart b/pkg/kernel/test/check_equivalence_test.dart
new file mode 100644
index 0000000..4996076
--- /dev/null
+++ b/pkg/kernel/test/check_equivalence_test.dart
@@ -0,0 +1,390 @@
+// Copyright (c) 2025, 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:expect/expect.dart';
+import 'package:kernel/ast.dart';
+import 'package:kernel/src/equivalence.dart' show EquivalenceResult;
+import 'package:kernel/src/tool/check_equivalence.dart';
+
+Uri fileUri1 = Uri.parse('file://uri1');
+Uri fileUri2 = Uri.parse('file://uri2');
+Uri fileUri3 = Uri.parse('file://uri3');
+Uri importUri1 = Uri.parse('import://uri1');
+Uri importUri2 = Uri.parse('import://uri2');
+Uri importUri3 = Uri.parse('import://uri3');
+
+List<Test> tests = [
+  new Test((_) => new Component()),
+  new Test((_) => new Component()
+    ..libraries.add(new Library(importUri1, fileUri: fileUri1))),
+  new Test((bool first) {
+    return new Component()
+      ..libraries.add(new Library(first ? importUri1 : importUri2,
+          fileUri: first ? fileUri1 : fileUri2))
+      ..libraries.add(new Library(first ? importUri2 : importUri1,
+          fileUri: first ? fileUri2 : fileUri1));
+  }, inequivalence: '''
+Inequivalent nodes
+1: library import://uri1
+2: library import://uri2
+.root
+ Component.libraries[0]
+''', unorderedLibraries: true),
+  new Test((bool first) {
+    Component c = new Component();
+    Library library1 = new Library(importUri1, fileUri: fileUri1);
+    Library library2 = new Library(importUri2, fileUri: fileUri2);
+    c.libraries.add(library1);
+    c.libraries.add(library2);
+    library1.dependencies
+      ..add(new LibraryDependency.import(first ? library2 : library1))
+      ..add(new LibraryDependency.import(first ? library1 : library2));
+    return c;
+  }, inequivalence: '''
+Inequivalent references:
+1: Reference to library import://uri2
+2: Reference to library import://uri1
+.root
+ Component.libraries[0]
+  Library(library import://uri1).dependencies[0]
+   LibraryDependency.importedLibraryReference
+Inequivalent references:
+1: Reference to library import://uri1
+2: Reference to library import://uri2
+.root
+ Component.libraries[0]
+  Library(library import://uri1).dependencies[1]
+   LibraryDependency.importedLibraryReference
+''', unorderedLibraryDependencies: true),
+  new Test((bool first) {
+    Component c = new Component();
+    Library l = new Library(importUri1, fileUri: fileUri1);
+    c.libraries.add(l);
+    Field f1 = new Field.immutable(new Name('field1'), fileUri: fileUri1);
+    l.addField(f1);
+    Field f2 = new Field.immutable(new Name('field2'), fileUri: fileUri1);
+    l.addField(f2);
+    l.additionalExports.add(first ? f1.getterReference : f2.getterReference);
+    l.additionalExports.add(first ? f2.getterReference : f1.getterReference);
+    return c;
+  }, inequivalence: '''
+Inequivalent references:
+1: Reference to field1
+2: Reference to field2
+.root
+ Component.libraries[0]
+  Library(library import://uri1).additionalExports[0]
+''', unorderedAdditionalExports: true),
+  new Test((bool first) {
+    Component c = new Component();
+    Library l = new Library(importUri1, fileUri: fileUri1);
+    c.libraries.add(l);
+    l.parts
+      ..add(new LibraryPart([], first ? '${fileUri2}' : '${fileUri3}')
+        ..parent = l)
+      ..add(new LibraryPart([], first ? '${fileUri3}' : '${fileUri2}')
+        ..parent = l);
+    return c;
+  }, inequivalence: '''
+Values file://uri2/ and file://uri3/ are not equivalent
+.root
+ Component.libraries[0]
+  Library(library import://uri1).parts[0]
+   LibraryPart.partUri
+Values file://uri3/ and file://uri2/ are not equivalent
+.root
+ Component.libraries[0]
+  Library(library import://uri1).parts[1]
+   LibraryPart.partUri
+''', unorderedParts: true),
+  new Test((bool first) {
+    Component c = new Component();
+    Library l = new Library(importUri1, fileUri: fileUri1);
+    c.libraries.add(l);
+    l
+      ..addTypedef(new Typedef(
+          first ? 'Typedef1' : 'Typedef2', const DynamicType(),
+          fileUri: fileUri1))
+      ..addTypedef(new Typedef(
+          first ? 'Typedef2' : 'Typedef1', const DynamicType(),
+          fileUri: fileUri1));
+    return c;
+  }, inequivalence: '''
+Inequivalent nodes
+1: Typedef(Typedef1)
+2: Typedef(Typedef2)
+.root
+ Component.libraries[0]
+  Library(library import://uri1).typedefs[0]
+''', unorderedTypedefs: true),
+  new Test((bool first) {
+    Component c = new Component();
+    Library l = new Library(importUri1, fileUri: fileUri1);
+    c.libraries.add(l);
+    l
+      ..addClass(
+          new Class(name: first ? 'Class1' : 'Class2', fileUri: fileUri1))
+      ..addClass(
+          new Class(name: first ? 'Class2' : 'Class1', fileUri: fileUri1));
+    return c;
+  }, inequivalence: '''
+Inequivalent nodes
+1: Class(Class1)
+2: Class(Class2)
+.root
+ Component.libraries[0]
+  Library(library import://uri1).classes[0]
+''', unorderedClasses: true),
+  new Test((bool first) {
+    Component c = new Component();
+    Library l = new Library(importUri1, fileUri: fileUri1);
+    c.libraries.add(l);
+    Field f1 = new Field.immutable(new Name('field1'), fileUri: fileUri1);
+    Field f2 = new Field.immutable(new Name('field2'), fileUri: fileUri1);
+    l.addField(first ? f1 : f2);
+    l.addField(first ? f2 : f1);
+    Class cls = new Class(name: first ? 'Class' : 'Class', fileUri: fileUri1);
+    l.addClass(cls);
+    Field f3 = new Field.immutable(new Name('field3'), fileUri: fileUri1);
+    Field f4 = new Field.immutable(new Name('field4'), fileUri: fileUri1);
+    cls.addField(first ? f3 : f4);
+    cls.addField(first ? f4 : f3);
+    return c;
+  }, inequivalence: '''
+Inequivalent nodes
+1: Class.field3
+2: Class.field4
+.root
+ Component.libraries[0]
+  Library(library import://uri1).classes[0]
+   Class(Class).fields[0]
+Inequivalent nodes
+1: field1
+2: field2
+.root
+ Component.libraries[0]
+  Library(library import://uri1).fields[0]
+''', unorderedFields: true),
+  new Test((bool first) {
+    Component c = new Component();
+    Library l = new Library(importUri1, fileUri: fileUri1);
+    c.libraries.add(l);
+    Procedure p1 = new Procedure(
+        new Name('procedure1'), ProcedureKind.Method, new FunctionNode(null),
+        fileUri: fileUri1);
+    Procedure p2 = new Procedure(
+        new Name('procedure2'), ProcedureKind.Method, new FunctionNode(null),
+        fileUri: fileUri1);
+    l.addProcedure(first ? p1 : p2);
+    l.addProcedure(first ? p2 : p1);
+    Class cls = new Class(name: first ? 'Class' : 'Class', fileUri: fileUri1);
+    l.addClass(cls);
+    Procedure p3 = new Procedure(
+        new Name('procedure3'), ProcedureKind.Method, new FunctionNode(null),
+        fileUri: fileUri1);
+    Procedure p4 = new Procedure(
+        new Name('procedure4'), ProcedureKind.Method, new FunctionNode(null),
+        fileUri: fileUri1);
+    cls.addProcedure(first ? p3 : p4);
+    cls.addProcedure(first ? p4 : p3);
+    return c;
+  }, inequivalence: '''
+Inequivalent nodes
+1: Class.procedure3
+2: Class.procedure4
+.root
+ Component.libraries[0]
+  Library(library import://uri1).classes[0]
+   Class(Class).procedures[0]
+Inequivalent nodes
+1: procedure1
+2: procedure2
+.root
+ Component.libraries[0]
+  Library(library import://uri1).procedures[0]
+''', unorderedProcedures: true),
+  new Test((bool first) {
+    Component c = new Component();
+    Library l = new Library(importUri1, fileUri: fileUri1);
+    c.libraries.add(l);
+    Class cls = new Class(name: first ? 'Class' : 'Class', fileUri: fileUri1);
+    l.addClass(cls);
+    Constructor c1 = new Constructor(new FunctionNode(null),
+        name: new Name('constructor1'), fileUri: fileUri1);
+    Constructor c2 = new Constructor(new FunctionNode(null),
+        name: new Name('constructor2'), fileUri: fileUri1);
+    cls.addConstructor(first ? c1 : c2);
+    cls.addConstructor(first ? c2 : c1);
+    return c;
+  }, inequivalence: '''
+Inequivalent nodes
+1: Class.constructor1
+2: Class.constructor2
+.root
+ Component.libraries[0]
+  Library(library import://uri1).classes[0]
+   Class(Class).constructors[0]
+''', unorderedConstructors: true),
+  new Test((bool first) {
+    Expression createAnnotation1() =>
+        first ? new IntLiteral(0) : new StringLiteral("foo");
+    Expression createAnnotation2() =>
+        first ? new StringLiteral("foo") : new IntLiteral(0);
+
+    Component c = new Component();
+    Library l = new Library(importUri1, fileUri: fileUri1);
+    c.libraries.add(l);
+    l
+      ..addAnnotation(createAnnotation1())
+      ..addAnnotation(createAnnotation2());
+    Class cls = new Class(name: first ? 'Class' : 'Class', fileUri: fileUri1);
+    l.addClass(cls);
+    cls
+      ..addAnnotation(createAnnotation1())
+      ..addAnnotation(createAnnotation2());
+    Procedure p = new Procedure(
+        new Name('procedure'), ProcedureKind.Method, new FunctionNode(null),
+        fileUri: fileUri1);
+    l.addProcedure(p);
+    p
+      ..addAnnotation(createAnnotation1())
+      ..addAnnotation(createAnnotation2());
+    return c;
+  }, inequivalence: '''
+Inequivalent nodes
+1: IntLiteral(0)
+2: StringLiteral("foo")
+.root
+ Component.libraries[0]
+  Library(library import://uri1).annotations[0]
+Inequivalent nodes
+1: IntLiteral(0)
+2: StringLiteral("foo")
+.root
+ Component.libraries[0]
+  Library(library import://uri1).classes[0]
+   Class(Class).annotations[0]
+Inequivalent nodes
+1: IntLiteral(0)
+2: StringLiteral("foo")
+.root
+ Component.libraries[0]
+  Library(library import://uri1).procedures[0]
+   Procedure(procedure).annotations[0]
+''', unorderedAnnotations: true),
+];
+
+class Test {
+  final Node a;
+  final Node b;
+  final String? inequivalence;
+  final bool? unorderedLibraries;
+  final bool? unorderedLibraryDependencies;
+  final bool? unorderedAdditionalExports;
+  final bool? unorderedParts;
+  final bool? unorderedTypedefs;
+  final bool? unorderedClasses;
+  final bool? unorderedFields;
+  final bool? unorderedProcedures;
+  final bool? unorderedConstructors;
+  final bool? unorderedAnnotations;
+
+  Test(
+    Node Function(bool) create, {
+    this.inequivalence,
+    this.unorderedLibraries,
+    this.unorderedLibraryDependencies,
+    this.unorderedAdditionalExports,
+    this.unorderedParts,
+    this.unorderedTypedefs,
+    this.unorderedClasses,
+    this.unorderedFields,
+    this.unorderedProcedures,
+    this.unorderedConstructors,
+    this.unorderedAnnotations,
+  })  : a = create(true),
+        b = create(false);
+
+  bool get isEquivalent => inequivalence == null;
+
+  String get options {
+    List<String> list = [];
+    if (unorderedLibraries != null) {
+      list.add('unorderedLibraries=$unorderedLibraries');
+    }
+    if (unorderedLibraryDependencies != null) {
+      list.add('unorderedLibraryDependencies=$unorderedLibraryDependencies');
+    }
+    if (unorderedAdditionalExports != null) {
+      list.add('unorderedAdditionalExports=$unorderedAdditionalExports');
+    }
+    if (unorderedParts != null) {
+      list.add('unorderedParts=$unorderedParts');
+    }
+    if (unorderedTypedefs != null) {
+      list.add('unorderedTypedefs=$unorderedTypedefs');
+    }
+    if (unorderedClasses != null) {
+      list.add('unorderedClasses=$unorderedClasses');
+    }
+    if (unorderedFields != null) {
+      list.add('unorderedFields=$unorderedFields');
+    }
+    if (unorderedProcedures != null) {
+      list.add('unorderedProcedures=$unorderedProcedures');
+    }
+    if (unorderedConstructors != null) {
+      list.add('unorderedConstructors=$unorderedConstructors');
+    }
+    if (unorderedAnnotations != null) {
+      list.add('unorderedAnnotations=$unorderedAnnotations');
+    }
+    return list.join(',');
+  }
+}
+
+void main() {
+  for (Test test in tests) {
+    EquivalenceResult result = checkNodeEquivalence(test.a, test.b);
+    if (test.isEquivalent) {
+      Expect.equals(result.isEquivalent, test.isEquivalent,
+          'Unexpected result for\n${test.a}\n${test.b}:\n$result');
+      Expect.equals(
+          '', test.options, 'Unexpected options for\n${test.a}\n${test.b}.');
+    } else if (result.isEquivalent) {
+      Expect.equals(
+          result.isEquivalent,
+          test.isEquivalent,
+          'Unexpected equivalence for\n${test.a}\n${test.b}:\n'
+          'Expected ${test.inequivalence}');
+    } else {
+      Expect.stringEquals(
+          result.toString(),
+          test.inequivalence!,
+          'Unexpected inequivalence result for\n${test.a}\n${test.b}:\n'
+          'Expected:\n---\n${test.inequivalence}\n---\n'
+          'Actual:\n---\n${result}\n---');
+
+      EquivalenceResult optionResult = checkNodeEquivalence(
+        test.a,
+        test.b,
+        unorderedLibraries: test.unorderedLibraries ?? false,
+        unorderedLibraryDependencies:
+            test.unorderedLibraryDependencies ?? false,
+        unorderedAdditionalExports: test.unorderedAdditionalExports ?? false,
+        unorderedParts: test.unorderedParts ?? false,
+        unorderedTypedefs: test.unorderedTypedefs ?? false,
+        unorderedClasses: test.unorderedClasses ?? false,
+        unorderedFields: test.unorderedFields ?? false,
+        unorderedProcedures: test.unorderedProcedures ?? false,
+        unorderedConstructors: test.unorderedConstructors ?? false,
+        unorderedAnnotations: test.unorderedAnnotations ?? false,
+      );
+      Expect.isTrue(
+          optionResult.isEquivalent,
+          'Unexpected result for\n${test.a}\n${test.b} with ${test.options}:\n'
+          '$optionResult');
+    }
+  }
+}