Add FeaturesDataInterpreter

Change-Id: I40a7baca235c2e729629f4330d8d51f148264ece
Reviewed-on: https://dart-review.googlesource.com/c/88963
Reviewed-by: Sigmund Cherem <sigmund@google.com>
Commit-Queue: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/compiler/lib/src/universe/use.dart b/pkg/compiler/lib/src/universe/use.dart
index 1336828..b5fc867 100644
--- a/pkg/compiler/lib/src/universe/use.dart
+++ b/pkg/compiler/lib/src/universe/use.dart
@@ -47,9 +47,9 @@
       var constraint = receiverConstraint;
       if (constraint is StrongModeConstraint) {
         if (constraint.isThis) {
-          sb.write('<');
+          sb.write('this:');
         } else if (constraint.isExact) {
-          sb.write('=');
+          sb.write('exact:');
         }
         sb.write(constraint.cls.name);
       } else {
diff --git a/pkg/compiler/lib/src/util/features.dart b/pkg/compiler/lib/src/util/features.dart
index 57aeba2..5263b40 100644
--- a/pkg/compiler/lib/src/util/features.dart
+++ b/pkg/compiler/lib/src/util/features.dart
@@ -25,9 +25,13 @@
     _features[key] = value;
   }
 
-  String operator [](String key) => _features[key];
+  Object operator [](String key) => _features[key];
 
-  String remove(String key) => _features.remove(key);
+  Object remove(String key) => _features.remove(key);
+
+  bool get isEmpty => _features.isEmpty;
+
+  bool get isNotEmpty => _features.isNotEmpty;
 
   void forEach(void Function(String, Object) f) {
     _features.forEach(f);
diff --git a/tests/compiler/dart2js/equivalence/id_equivalence_helper.dart b/tests/compiler/dart2js/equivalence/id_equivalence_helper.dart
index f9b8ebf..e7becbd 100644
--- a/tests/compiler/dart2js/equivalence/id_equivalence_helper.dart
+++ b/tests/compiler/dart2js/equivalence/id_equivalence_helper.dart
@@ -11,6 +11,7 @@
 import 'package:compiler/src/commandline_options.dart';
 import 'package:compiler/src/compiler.dart';
 import 'package:compiler/src/elements/entities.dart';
+import 'package:compiler/src/util/features.dart';
 import 'package:expect/expect.dart';
 import 'package:sourcemap_testing/src/annotated_code_helper.dart';
 
@@ -687,6 +688,87 @@
   }
 }
 
+class FeaturesDataInterpreter implements DataInterpreter<Features> {
+  const FeaturesDataInterpreter();
+
+  @override
+  String isAsExpected(Features actualFeatures, String expectedData) {
+    if (expectedData == '*') {
+      return null;
+    } else if (expectedData == '') {
+      return actualFeatures.isNotEmpty ? "Expected empty data." : null;
+    } else {
+      List<String> errorsFound = [];
+      Features expectedFeatures = Features.fromText(expectedData);
+      expectedFeatures.forEach((String key, Object expectedValue) {
+        Object actualValue = actualFeatures[key] ?? '';
+        if (expectedValue == '') {
+          if (actualValue != '') {
+            errorsFound.add('Non-empty data found for $key');
+          }
+        } else if (expectedValue == '*') {
+          return;
+        } else if (expectedValue is List) {
+          if (actualValue is List) {
+            List actualList = actualValue.toList();
+            for (Object expectedObject in expectedValue) {
+              String expectedText = '$expectedObject';
+              bool matchFound = false;
+              if (expectedText.endsWith('*')) {
+                // Wildcard matcher.
+                String prefix =
+                    expectedText.substring(0, expectedText.indexOf('*'));
+                List matches = [];
+                for (Object actualObject in actualList) {
+                  if ('$actualObject'.startsWith(prefix)) {
+                    matches.add(actualObject);
+                    matchFound = true;
+                  }
+                }
+                for (Object match in matches) {
+                  actualList.remove(match);
+                }
+              } else {
+                for (Object actualObject in actualList) {
+                  if (expectedText == '$actualObject') {
+                    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}");
+        }
+      });
+      return errorsFound.isNotEmpty ? errorsFound.join(', ') : null;
+    }
+  }
+
+  @override
+  String getText(Features actualData) {
+    return actualData.getText();
+  }
+
+  @override
+  bool isEmpty(Features actualData) {
+    return actualData == null || actualData.isEmpty;
+  }
+}
+
 /// Checks [compiledData] against the expected data in [expectedMap] derived
 /// from [code].
 Future<bool> checkCode<T>(
@@ -715,8 +797,9 @@
           reportError(
               data.compiler.reporter,
               actualData.sourceSpan,
-              'EXTRA $mode DATA for ${id.descriptor} = '
-              '${colorizeActual('${IdValue.idToString(id, actualText)}')} for ${actualData.objectText}. '
+              'EXTRA $mode DATA for ${id.descriptor}:\n '
+              'object   : ${actualData.objectText}\n '
+              'actual   : ${colorizeActual('${IdValue.idToString(id, actualText)}')}\n '
               'Data was expected for these ids: ${expectedMap.keys}');
           if (filterActualData == null || filterActualData(null, actualData)) {
             hasLocalFailure = true;
@@ -730,7 +813,7 @@
           reportError(
               data.compiler.reporter,
               actualData.sourceSpan,
-              'UNEXPECTED $mode DATA for ${id.descriptor}: \n '
+              'UNEXPECTED $mode DATA for ${id.descriptor}:\n '
               'detail  : ${colorizeMessage(unexpectedMessage)}\n '
               'object  : ${actualData.objectText}\n '
               'expected: ${colorizeExpected('$expected')}\n '
diff --git a/tests/compiler/dart2js/impact/data/runtime_type.dart b/tests/compiler/dart2js/impact/data/runtime_type.dart
index 32fa0a7..b287715 100644
--- a/tests/compiler/dart2js/impact/data/runtime_type.dart
+++ b/tests/compiler/dart2js/impact/data/runtime_type.dart
@@ -5,7 +5,7 @@
 /*element: Class1a.:static=[Object.(0)]*/
 class Class1a<T> {
   /*element: Class1a.==:
-   dynamic=[<Class1a.runtimeType,Object.runtimeType,Type.==],
+   dynamic=[this:Class1a.runtimeType,Object.runtimeType,Type.==],
    runtimeType=[equals:Class1a<Class1a.T>/dynamic]
   */
   bool operator ==(other) {
@@ -16,7 +16,7 @@
 /*element: Class1b.:static=[Class1a.(0)]*/
 class Class1b<T> extends Class1a<T> {
   /*element: Class1b.==:
-   dynamic=[<Class1b.runtimeType,Object.runtimeType,Type.==],
+   dynamic=[this:Class1b.runtimeType,Object.runtimeType,Type.==],
    runtimeType=[equals:dynamic/Class1b<Class1b.T>]
   */
   bool operator ==(other) {
@@ -27,7 +27,7 @@
 /*element: Class1c.:static=[Object.(0)]*/
 class Class1c<T> implements Class1a<T> {
   /*element: Class1c.==:
-   dynamic=[<Class1c.runtimeType,Object.==,Object.runtimeType,Type.==],
+   dynamic=[this:Class1c.runtimeType,Object.==,Object.runtimeType,Type.==],
    runtimeType=[equals:Class1c<Class1c.T>/dynamic],
    type=[inst:JSNull]
   */
@@ -39,7 +39,7 @@
 /*element: Class1d.:static=[Object.(0)]*/
 class Class1d<T> implements Class1a<T> {
   /*element: Class1d.==:
-   dynamic=[<Class1d.runtimeType,Object.==,Object.runtimeType,Type.==],
+   dynamic=[this:Class1d.runtimeType,Object.==,Object.runtimeType,Type.==],
    runtimeType=[equals:dynamic/Class1d<Class1d.T>],
    type=[inst:JSNull]
   */
diff --git a/tests/compiler/dart2js/impact/data/this.dart b/tests/compiler/dart2js/impact/data/this.dart
index 2579dda..73a03d1 100644
--- a/tests/compiler/dart2js/impact/data/this.dart
+++ b/tests/compiler/dart2js/impact/data/this.dart
@@ -10,12 +10,12 @@
   /*element: Class.field2:type=[inst:JSNull]*/
   var field2;
 
-  /*element: Class.method1:dynamic=[<Class.method2(0)]*/
+  /*element: Class.method1:dynamic=[this:Class.method2(0)]*/
   method1() {
     method2();
   }
 
-  /*element: Class.method2:dynamic=[<Class.field1=,<Class.field2]*/
+  /*element: Class.method2:dynamic=[this:Class.field1=,this:Class.field2]*/
   method2() {
     field1 = field2;
   }
@@ -31,7 +31,7 @@
   /*element: Subclass.method1:*/
   method1() {}
 
-  /*element: Subclass.method2:dynamic=[<Subclass.method3(0)]*/
+  /*element: Subclass.method2:dynamic=[this:Subclass.method3(0)]*/
   method2() {
     method3();
   }
diff --git a/tests/compiler/dart2js/impact/impact_test.dart b/tests/compiler/dart2js/impact/impact_test.dart
index d1cc0b1b..e59b077 100644
--- a/tests/compiler/dart2js/impact/impact_test.dart
+++ b/tests/compiler/dart2js/impact/impact_test.dart
@@ -33,12 +33,12 @@
   static const String runtimeTypeUse = 'runtimeType';
 }
 
-class ImpactDataComputer extends DataComputer<String> {
+class ImpactDataComputer extends DataComputer<Features> {
   const ImpactDataComputer();
 
   @override
   void computeMemberData(Compiler compiler, MemberEntity member,
-      Map<Id, ActualData<String>> actualMap,
+      Map<Id, ActualData<Features>> actualMap,
       {bool verbose: false}) {
     KernelFrontEndStrategy frontendStrategy = compiler.frontendStrategy;
     WorldImpact impact = compiler.impactCache[member];
@@ -72,10 +72,11 @@
       }
     }
     Id id = computeEntityId(node);
-    actualMap[id] = new ActualData<String>(
-        id, features.getText(), computeSourceSpanFromTreeNode(node), member);
+    actualMap[id] = new ActualData<Features>(
+        id, features, computeSourceSpanFromTreeNode(node), member);
   }
 
   @override
-  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
+  DataInterpreter<Features> get dataValidator =>
+      const FeaturesDataInterpreter();
 }