[dart2js] Adding kernel dump info regression tests

Ensures that kernel dump info doesn't break the original dump info's behavior.

Change-Id: I9bfe22cea0d1a5597db9c98fe736f20369b80c82
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/247122
Reviewed-by: Joshua Litt <joshualitt@google.com>
Commit-Queue: Mark Zhou <markzipan@google.com>
diff --git a/pkg/compiler/lib/src/dump_info.dart b/pkg/compiler/lib/src/dump_info.dart
index 163bc4d..8f700cb 100644
--- a/pkg/compiler/lib/src/dump_info.dart
+++ b/pkg/compiler/lib/src/dump_info.dart
@@ -572,7 +572,7 @@
     bool isGetter = parent is ir.Procedure && parent.isGetter;
     bool isSetter = parent is ir.Procedure && parent.isSetter;
     int kind;
-    if (isTopLevel) {
+    if (isStaticMember || isTopLevel) {
       kind = FunctionInfo.TOP_LEVEL_FUNCTION_KIND;
     } else if (isMethod) {
       kind = FunctionInfo.METHOD_FUNCTION_KIND;
diff --git a/pkg/compiler/test/dump_info/data/marker.options b/pkg/compiler/test/dump_info/data/marker.options
index 620f7d3..e127a4b 100644
--- a/pkg/compiler/test/dump_info/data/marker.options
+++ b/pkg/compiler/test/dump_info/data/marker.options
@@ -1,2 +1,3 @@
 spec=pkg/compiler/test/dump_info/dump_info_test.dart
+canary=pkg/compiler/test/dump_info/dump_info_new_regression_test.dart
 
diff --git a/pkg/compiler/test/dump_info/dump_info_new_regression_test.dart b/pkg/compiler/test/dump_info/dump_info_new_regression_test.dart
new file mode 100644
index 0000000..afcd05d
--- /dev/null
+++ b/pkg/compiler/test/dump_info/dump_info_new_regression_test.dart
@@ -0,0 +1,349 @@
+// 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.
+
+// Tests that Kernel Dump Info matches the original Dump Info's output after
+// filtering non-live blocks.
+
+// @dart = 2.7
+
+import 'dart:convert';
+import 'dart:io';
+import 'package:_fe_analyzer_shared/src/testing/features.dart';
+import 'package:async_helper/async_helper.dart';
+import 'package:compiler/src/compiler.dart';
+import 'package:compiler/src/dump_info.dart';
+import 'package:compiler/src/elements/entities.dart';
+import 'package:compiler/src/js_model/element_map.dart';
+import 'package:compiler/src/js_model/js_world.dart';
+import 'package:dart2js_info/info.dart' as info;
+import 'package:dart2js_info/json_info_codec.dart' as info;
+import 'package:kernel/ast.dart' as ir;
+import '../equivalence/id_equivalence.dart';
+import '../equivalence/id_equivalence_helper.dart';
+
+final JsonEncoder encoder = const JsonEncoder();
+final JsonEncoder indentedEncoder = const JsonEncoder.withIndent('  ');
+
+String jsonEncode(Map object, {bool indent = true}) {
+  var jsonEncoder = indent ? indentedEncoder : encoder;
+  var transformedObject = transformJsonObjectForComparison(object);
+  // Filter block comments since they interfere with ID test comments.
+  var json = jsonEncoder
+      .convert(transformedObject)
+      .replaceAll('/*', '')
+      .replaceAll('*/', '');
+  return json;
+}
+
+/// Transforms a new DumpInfo or old DumpInfo object into a format that allows
+/// for easier comparison.
+Map transformJsonObjectForComparison(Map object) {
+  Map newObject = {};
+  object.forEach((key, value) {
+    if (value == null) {
+      newObject[key] = value;
+      return;
+    }
+
+    dynamic newValue = value;
+    // Ignore type fields since K-World and J-World type strings are
+    // non-trivially different (though semantically identical).
+    if (key == 'type' || key == 'returnType') {
+      return;
+    }
+
+    // Ignore sizes for output units, as these deviate by some constant when
+    // `--canary` is enabled.
+    if (object['kind'] == 'outputUnit' && key == 'size') {
+      return;
+    }
+
+    // Remove disambiguation portions of names. E.g., name%N -> name.
+    if (key == 'id' || key == 'name' || key == 'function') {
+      newValue = value.replaceAll(RegExp(r'%\d+'), '');
+    } else if (key == 'children') {
+      List values = object[key];
+      newValue =
+          values.map((name) => name.replaceAll(RegExp(r'%\d+'), '')).toList();
+    }
+
+    newObject[key] = newValue;
+  });
+  return newObject;
+}
+
+Map filteredJsonObject(Map object, Set<String> filteredFields) {
+  Map filteredObject = {};
+  object.forEach((key, value) {
+    if (filteredFields.contains(key)) return;
+    filteredObject[key] = value;
+  });
+  return filteredObject;
+}
+
+void main(List<String> args) {
+  // Do not allow these tests to regenerate annotations.
+  final filteredArgs =
+      args.where((arg) => arg != '--g' && arg != '--generate').toList();
+
+  asyncTest(() async {
+    Directory dataDir = Directory.fromUri(Platform.script.resolve('data'));
+    print('Testing output of dump-info');
+    print('==================================================================');
+    await checkTests(dataDir, const DumpInfoDataComputer(),
+        args: filteredArgs,
+        testedConfigs: allSpecConfigs,
+        options: ['--dump-info', '--canary', '--enable-asserts']);
+  });
+}
+
+class Tags {
+  static const String library = 'library';
+  static const String clazz = 'class';
+  static const String classType = 'classType';
+  static const String closure = 'closure';
+  static const String function = 'function';
+  static const String typeDef = 'typedef';
+  static const String field = 'field';
+  static const String constant = 'constant';
+  static const String holding = 'holding';
+  static const String dependencies = 'dependencies';
+  static const String outputUnits = 'outputUnits';
+  static const String deferredFiles = 'deferredFiles';
+}
+
+class DumpInfoDataComputer extends DataComputer<Features> {
+  const DumpInfoDataComputer();
+
+  static const String wildcard = '%';
+
+  @override
+  void computeLibraryData(Compiler compiler, LibraryEntity library,
+      Map<Id, ActualData<Features>> actualMap,
+      {bool verbose}) {
+    final converter = info.AllInfoToJsonConverter(isBackwardCompatible: true);
+    DumpInfoStateData dumpInfoState = compiler.dumpInfoStateForTesting;
+    TreeShakingInfoVisitor().filter(dumpInfoState.info);
+
+    final features = Features();
+    final libraryInfo = dumpInfoState.entityToInfo[library];
+    if (libraryInfo == null) return;
+
+    features.addElement(
+        Tags.library, jsonEncode(libraryInfo.accept(converter)));
+
+    // Store program-wide information on the main library.
+    final name = '${library.canonicalUri.pathSegments.last}';
+    if (name.startsWith('main')) {
+      for (final constantInfo in dumpInfoState.info.constants) {
+        features.addElement(
+            Tags.constant, jsonEncode(constantInfo.accept(converter)));
+      }
+      features.addElement(
+          Tags.dependencies, jsonEncode(dumpInfoState.info.dependencies));
+      for (final outputUnit in dumpInfoState.info.outputUnits) {
+        var outputUnitJsonObject = outputUnit.accept(converter);
+        // Remove the size from output units due to high noise ratio.
+        outputUnitJsonObject =
+            filteredJsonObject(outputUnitJsonObject, {'size'});
+        features.addElement(Tags.outputUnits, jsonEncode(outputUnitJsonObject));
+      }
+      features.addElement(
+          Tags.deferredFiles, jsonEncode(dumpInfoState.info.deferredFiles));
+    }
+
+    final id = LibraryId(library.canonicalUri);
+    actualMap[id] =
+        ActualData<Features>(id, features, library.canonicalUri, -1, library);
+  }
+
+  @override
+  void computeClassData(Compiler compiler, ClassEntity cls,
+      Map<Id, ActualData<Features>> actualMap,
+      {bool verbose: false}) {
+    final converter = info.AllInfoToJsonConverter(isBackwardCompatible: true);
+    DumpInfoStateData dumpInfoState = compiler.dumpInfoStateForTesting;
+    TreeShakingInfoVisitor().filter(dumpInfoState.info);
+
+    final features = Features();
+    final classInfo = dumpInfoState.entityToInfo[cls];
+    if (classInfo == null) return;
+
+    features.addElement(Tags.clazz, jsonEncode(classInfo.accept(converter)));
+    final classTypeInfos =
+        dumpInfoState.info.classTypes.where((i) => i.name == classInfo.name);
+    assert(
+        classTypeInfos.length < 2,
+        'Ambiguous class type info resolution. '
+        'Expected 0 or 1 elements, found: $classTypeInfos');
+    if (classTypeInfos.length == 1) {
+      features.addElement(
+          Tags.classType, jsonEncode(classTypeInfos.first.accept(converter)));
+    }
+
+    JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
+    JsToElementMap elementMap = closedWorld.elementMap;
+    ir.Class node = elementMap.getClassDefinition(cls).node;
+    ClassId id = ClassId(node.name);
+    ir.TreeNode nodeWithOffset = computeTreeNodeWithOffset(node);
+    actualMap[id] = ActualData<Features>(id, features,
+        nodeWithOffset?.location?.file, nodeWithOffset?.fileOffset, cls);
+  }
+
+  @override
+  void computeMemberData(Compiler compiler, MemberEntity member,
+      Map<Id, ActualData<Features>> actualMap,
+      {bool verbose: false}) {
+    final converter = info.AllInfoToJsonConverter(isBackwardCompatible: true);
+    DumpInfoStateData dumpInfoState = compiler.dumpInfoStateForTesting;
+    TreeShakingInfoVisitor().filter(dumpInfoState.info);
+
+    final features = Features();
+    final functionInfo = dumpInfoState.entityToInfo[member];
+    if (functionInfo == null ||
+        functionInfo.treeShakenStatus != info.TreeShakenStatus.Live) return;
+
+    if (functionInfo is info.FunctionInfo) {
+      features.addElement(
+          Tags.function, jsonEncode(functionInfo.accept(converter)));
+      for (final use in functionInfo.uses) {
+        features.addElement(Tags.holding,
+            jsonEncode(converter.visitDependencyInfo(use), indent: false));
+      }
+      for (final closure in functionInfo.closures) {
+        features.addElement(
+            Tags.closure, jsonEncode(closure.accept(converter)));
+      }
+    }
+
+    if (functionInfo is info.FieldInfo) {
+      features.addElement(
+          Tags.function, jsonEncode(functionInfo.accept(converter)));
+      for (final use in functionInfo.uses) {
+        features.addElement(Tags.holding,
+            jsonEncode(converter.visitDependencyInfo(use), indent: false));
+      }
+      for (final closure in functionInfo.closures) {
+        features.addElement(
+            Tags.closure, jsonEncode(closure.accept(converter)));
+      }
+    }
+
+    JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
+    JsToElementMap elementMap = closedWorld.elementMap;
+    ir.Member node = elementMap.getMemberDefinition(member).node;
+    Id id = computeMemberId(node);
+    ir.TreeNode nodeWithOffset = computeTreeNodeWithOffset(node);
+    actualMap[id] = ActualData<Features>(id, features,
+        nodeWithOffset?.location?.file, nodeWithOffset?.fileOffset, member);
+  }
+
+  @override
+  DataInterpreter<Features> get dataValidator =>
+      const JsonFeaturesDataInterpreter(wildcard: wildcard);
+}
+
+/// Feature interpreter for Features with Json values.
+///
+/// The data annotation reader conserves whitespace visually while ignoring
+/// them during comparison.
+class JsonFeaturesDataInterpreter implements DataInterpreter<Features> {
+  final String wildcard;
+  const JsonFeaturesDataInterpreter({this.wildcard});
+
+  @override
+  String isAsExpected(Features actualFeatures, String expectedData) {
+    if (wildcard != null && expectedData == wildcard) {
+      return null;
+    } else if (expectedData == '') {
+      return actualFeatures.isNotEmpty ? "Expected empty data." : null;
+    } else {
+      List<String> errorsFound = [];
+      Features expectedFeatures = Features.fromText(expectedData);
+      Set<String> validatedFeatures = Set<String>();
+      expectedFeatures.forEach((String key, Object expectedValue) {
+        validatedFeatures.add(key);
+        Object actualValue = actualFeatures[key];
+        if (!actualFeatures.containsKey(key)) {
+          errorsFound.add('No data found for $key');
+        } else if (expectedValue == '') {
+          if (actualValue != '') {
+            errorsFound.add('Non-empty data found for $key');
+          }
+        } else if (wildcard != null && expectedValue == wildcard) {
+          return;
+        } else if (expectedValue is List) {
+          if (actualValue is List) {
+            List actualList = actualValue.toList();
+            for (Object expectedObject in expectedValue) {
+              String expectedText =
+                  jsonEncode(jsonDecode(expectedObject), indent: false);
+              bool matchFound = false;
+              if (wildcard != null && expectedText.endsWith(wildcard)) {
+                // Wildcard matcher.
+                String prefix =
+                    expectedText.substring(0, expectedText.indexOf(wildcard));
+                List matches = [];
+                for (Object actualObject in actualList) {
+                  final formattedActualObject =
+                      jsonEncode(jsonDecode(actualObject), indent: false);
+                  if (formattedActualObject.startsWith(prefix)) {
+                    matches.add(actualObject);
+                    matchFound = true;
+                  }
+                }
+                for (Object match in matches) {
+                  actualList.remove(match);
+                }
+              } else {
+                for (Object actualObject in actualList) {
+                  final formattedActualObject =
+                      jsonEncode(jsonDecode(actualObject), indent: false);
+                  if (expectedText == formattedActualObject) {
+                    actualList.remove(actualObject);
+                    matchFound = true;
+                    break;
+                  }
+                }
+              }
+              if (!matchFound) {
+                errorsFound.add("No match found for $key=[$expectedText]");
+              }
+            }
+            if (actualList.isNotEmpty) {
+              errorsFound
+                  .add("Extra data found $key=[${actualList.join(',')}]");
+            }
+          } else {
+            errorsFound.add("List data expected for $key: "
+                "expected '$expectedValue', found '${actualValue}'");
+          }
+        } else if (expectedValue != actualValue) {
+          errorsFound.add("Mismatch for $key: expected '$expectedValue', "
+              "found '${actualValue}'");
+        }
+      });
+      actualFeatures.forEach((String key, Object value) {
+        if (!validatedFeatures.contains(key)) {
+          if (value == '') {
+            errorsFound.add("Extra data found '$key'");
+          } else {
+            errorsFound.add("Extra data found $key=$value");
+          }
+        }
+      });
+      return errorsFound.isNotEmpty ? errorsFound.join('\n ') : null;
+    }
+  }
+
+  @override
+  String getText(Features actualData, [String indentation]) {
+    return actualData.getText(indentation);
+  }
+
+  @override
+  bool isEmpty(Features actualData) {
+    return actualData == null || actualData.isEmpty;
+  }
+}
diff --git a/pkg/dart2js_info/lib/info.dart b/pkg/dart2js_info/lib/info.dart
index cc97879..48ae501 100644
--- a/pkg/dart2js_info/lib/info.dart
+++ b/pkg/dart2js_info/lib/info.dart
@@ -74,8 +74,6 @@
 }
 
 /// The entire information produced while compiling a program.
-// TODO(markzipan): Make more of these fields final. These are mutated by the
-// Kernel Dump Info tests, but there may be a better pattern here.
 class AllInfo {
   /// Summary information about the program.
   ProgramInfo? program;