[kernel] Add tool for comparing class hierarchies of two dills

This CL adds a tool that can compare the class hierarchies of two dill files.
Currently it only checks for public classes and their public supertypes.
We could extend the tool later if needed.

Currently it gives this for flutter:

```
$ out/ReleaseX64/dart pkg/kernel/bin/compare_hierarchies.dart /path/to/flutter/bin/cache/flutter_web_sdk/kernel/flutter_ddc_sdk_sound.dill /path/to/flutter/bin/cache/artifacts/engine/common/flutter_patched_sdk/platform_strong.dill "dart:nativewrappers#NativeFieldWrapperClass1"
(1): /path/to/flutter/bin/cache/flutter_web_sdk/kernel/flutter_ddc_sdk_sound.dill
(2): /path/to/flutter/bin/cache/artifacts/engine/common/flutter_patched_sdk/platform_strong.dill

Missing classes in lib dart:_internal
In (2) but not in (1): [VMLibraryHooks, Lists, VMInternalsForTesting, ClassID]

Missing classes in lib dart:ui
In (1) but not in (2): [PlatformViewRegistry]

Class(ColorFilter) in dart.ui from (2) has these extra supertypes: [ImageFilter (dart:ui)]
```

where the last line is https://github.com/dart-lang/sdk/issues/48245

It gives this for the dart sdk:
```
$ out/ReleaseX64/dart pkg/kernel/bin/compare_hierarchies.dart out/ReleaseX64/vm_platform_strong.dill out/ReleaseX64/ddc_platform_sound.dill "dart:nativewrappers#NativeFieldWrapperClass1"
(1): out/ReleaseX64/vm_platform_strong.dill
(2): out/ReleaseX64/ddc_platform_sound.dill

Missing classes in lib dart:_internal
In (1) but not in (2): [VMLibraryHooks, Lists, VMInternalsForTesting, ClassID]
```

which seems fine (dart:_internal after all)

Change-Id: I03c0dbcc3e5058a0c7d6a996b511cf6d449dd4f3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/231100
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Commit-Queue: Jens Johansen <jensj@google.com>
diff --git a/pkg/kernel/bin/compare_hierarchies.dart b/pkg/kernel/bin/compare_hierarchies.dart
new file mode 100644
index 0000000..2097aaa
--- /dev/null
+++ b/pkg/kernel/bin/compare_hierarchies.dart
@@ -0,0 +1,162 @@
+#!/usr/bin/env dart
+// Copyright (c) 2022, 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:kernel/class_hierarchy.dart';
+import 'package:kernel/core_types.dart';
+import 'package:kernel/kernel.dart';
+import 'package:kernel/src/tool/command_line_util.dart';
+
+void usage() {
+  print("Compares the hierarchies of two dill files.");
+  print("");
+  print("Usage: dart <script> dillFile1.dill dillFile2.dill "
+      "[import:uri#classToIgnore|another:uri#andClassToIgnore]");
+  exit(1);
+}
+
+void main(List<String> args) {
+  CommandLineHelper.requireVariableArgumentCount([2, 3], args, usage);
+  CommandLineHelper.requireFileExists(args[0]);
+  CommandLineHelper.requireFileExists(args[1]);
+  Component binary1 = CommandLineHelper.tryLoadDill(args[0]);
+  Component binary2 = CommandLineHelper.tryLoadDill(args[1]);
+  Map<Uri, Set<String>> ignoresMap = {};
+  if (args.length >= 3) {
+    List<String> ignores = args[2].split("|");
+    for (String ignore in ignores) {
+      List<String> uriClassName = ignore.split("#");
+      if (uriClassName.length != 2) {
+        print("Ignoring '$ignore' as it doesn't conform to "
+            "'importUri#className'");
+        continue;
+      }
+      Uri uri = Uri.parse(uriClassName[0]);
+      String className = uriClassName[1];
+      (ignoresMap[uri] ??= {}).add(className);
+    }
+  }
+
+  print("(1): ${args[0]}");
+  print("(2): ${args[1]}");
+  print("");
+
+  ClosedWorldClassHierarchy ch1 =
+      new ClassHierarchy(binary1, new CoreTypes(binary1))
+          as ClosedWorldClassHierarchy;
+  ClosedWorldClassHierarchy ch2 =
+      new ClassHierarchy(binary2, new CoreTypes(binary2))
+          as ClosedWorldClassHierarchy;
+
+  Map<Uri, Library> libMap1 = createLibMap(binary1);
+  Map<Uri, Library> libMap2 = createLibMap(binary2);
+  Set<Uri> agreeingImportUris = new Set<Uri>.from(libMap1.keys)
+    ..retainAll(libMap2.keys);
+
+  for (Uri uri in agreeingImportUris) {
+    Library lib1 = libMap1[uri]!;
+    Library lib2 = libMap2[uri]!;
+    Map<String, Class> libClass1 =
+        createPublicClassMap(lib1, ignored: ignoresMap[uri]);
+    Map<String, Class> libClass2 =
+        createPublicClassMap(lib2, ignored: ignoresMap[uri]);
+    Set<String> agreeingClasses = new Set<String>.from(libClass1.keys)
+      ..retainAll(libClass2.keys);
+    if (agreeingClasses.length != libClass1.length ||
+        libClass1.length != libClass2.length) {
+      print("Missing classes in lib $uri");
+      Set<String> missing = new Set<String>.from(libClass1.keys)
+        ..removeAll(libClass2.keys);
+      if (missing.isNotEmpty) {
+        print("In (1) but not in (2): ${missing.toList()}");
+      }
+      missing = new Set<String>.from(libClass2.keys)..removeAll(libClass1.keys);
+      if (missing.isNotEmpty) {
+        print("In (2) but not in (1): ${missing.toList()}");
+      }
+      print("");
+    }
+
+    for (String className in agreeingClasses) {
+      Class c1 = libClass1[className]!;
+      Class c2 = libClass2[className]!;
+      Set<Classish> c1Supertypes = createClassishSet(
+          ch1.getAllSupertypeClassesForTesting(c1),
+          onlyPublic: true,
+          ignoresMap: ignoresMap);
+      Set<Classish> c2Supertypes = createClassishSet(
+          ch2.getAllSupertypeClassesForTesting(c2),
+          onlyPublic: true,
+          ignoresMap: ignoresMap);
+      Set<Classish> missing = new Set<Classish>.from(c1Supertypes)
+        ..removeAll(c2Supertypes);
+      if (missing.isNotEmpty) {
+        print("$c1 in $lib1 from (1) has these extra supertypes: "
+            "${missing.toList()}");
+      }
+      missing = new Set<Classish>.from(c2Supertypes)..removeAll(c1Supertypes);
+      if (missing.isNotEmpty) {
+        print("$c2 in $lib2 from (2) has these extra supertypes: "
+            "${missing.toList()}");
+      }
+    }
+  }
+}
+
+Map<Uri, Library> createLibMap(Component c) {
+  Map<Uri, Library> map = {};
+  for (Library lib in c.libraries) {
+    map[lib.importUri] = lib;
+  }
+  return map;
+}
+
+Map<String, Class> createPublicClassMap(Library lib,
+    {required Set<String>? ignored}) {
+  Map<String, Class> map = {};
+  for (Class c in lib.classes) {
+    if (c.name.startsWith("_")) continue;
+    if (ignored?.contains(c.name) ?? false) continue;
+    map[c.name] = c;
+  }
+  return map;
+}
+
+Set<Classish> createClassishSet(List<Class> classes,
+    {required bool onlyPublic, required Map<Uri, Set<String>> ignoresMap}) {
+  Set<Classish> result = {};
+  for (Class c in classes) {
+    if (onlyPublic && c.name.startsWith("_")) continue;
+    Set<String>? ignored = ignoresMap[c.enclosingLibrary.importUri];
+    if (ignored?.contains(c.name) ?? false) continue;
+    result.add(new Classish(c.name, c.enclosingLibrary.importUri));
+  }
+  return result;
+}
+
+class Classish {
+  final String name;
+  final Uri libImportUri;
+
+  const Classish(this.name, this.libImportUri);
+
+  @override
+  int get hashCode => name.hashCode * 13 + libImportUri.hashCode * 17;
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+    if (other is! Classish) return false;
+    if (name != other.name) return false;
+    if (libImportUri != other.libImportUri) return false;
+    return true;
+  }
+
+  @override
+  String toString() {
+    return "$name ($libImportUri)";
+  }
+}
diff --git a/pkg/kernel/lib/class_hierarchy.dart b/pkg/kernel/lib/class_hierarchy.dart
index cf9320c..440da58 100644
--- a/pkg/kernel/lib/class_hierarchy.dart
+++ b/pkg/kernel/lib/class_hierarchy.dart
@@ -491,6 +491,25 @@
     return result;
   }
 
+  List<Class> getAllSupertypeClassesForTesting(Class class_) {
+    List<Class?> allClassesByIndex =
+        new List<Class?>.filled(_infoMap.length, null);
+    for (MapEntry<Class, _ClassInfo> c in _infoMap.entries) {
+      allClassesByIndex[c.value.topologicalIndex] = c.key;
+    }
+
+    List<Class> result = [];
+    Uint32List list = _infoMap[class_]!.supertypeIntervalList;
+    for (int i = 0; i < list.length; i += 2) {
+      int from = list[i];
+      int to = list[i + 1];
+      for (int j = from; j < to; j++) {
+        result.add(allClassesByIndex[j]!);
+      }
+    }
+    return result;
+  }
+
   _ClassInfo infoFor(Class cls) {
     _ClassInfo? info = _infoMap[cls];
     if (info == null) {