Begin writing code to compare analyzer and front_end behaviors.

This will help us ensure that the analyzer and the front end interpret
programs in the same way, e.g. they should produce the same type
inference results.

This code is in its own package for now to avoid making the analyzer
package unnecessarily depend on the details of the kernel
representation (which would couple it even more tightly to kernel than
it's already coupled).  We can move it into the analyzer later if we
see fit.

Change-Id: I055e83c7b83b6cd9b1082b4424a2f4144b5abf0e
Reviewed-on: https://dart-review.googlesource.com/72480
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
diff --git a/.packages b/.packages
index b459b60..fe03169 100644
--- a/.packages
+++ b/.packages
@@ -10,6 +10,7 @@
 analysis_server_client:pkg/analysis_server_client/lib
 analyzer:pkg/analyzer/lib
 analyzer_cli:pkg/analyzer_cli/lib
+analyzer_fe_comparison:pkg/analyzer_fe_comparison/lib
 analyzer_plugin:pkg/analyzer_plugin/lib
 args:third_party/pkg/args/lib
 async:third_party/pkg/async/lib
diff --git a/pkg/analyzer_fe_comparison/lib/comparison.dart b/pkg/analyzer_fe_comparison/lib/comparison.dart
new file mode 100644
index 0000000..03138b7
--- /dev/null
+++ b/pkg/analyzer_fe_comparison/lib/comparison.dart
@@ -0,0 +1,23 @@
+// 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:analyzer_fe_comparison/src/analyzer.dart';
+import 'package:analyzer_fe_comparison/src/comparison_node.dart';
+import 'package:analyzer_fe_comparison/src/kernel.dart';
+
+/// Compares the analyzer and kernel representations of a project, and prints
+/// the resulting diff.
+void compare(
+    String platformPath, String projectLibPath, String packagesFilePath) async {
+  ComparisonNode analyzerNode = await driveAnalyzer(projectLibPath);
+  var packagesFileUri = Uri.file(packagesFilePath);
+  var inputs = <Uri>[];
+  for (var library in analyzerNode.children) {
+    inputs.add(Uri.parse(library.text));
+  }
+  var platformUri = Uri.file(platformPath);
+  ComparisonNode kernelNode =
+      await driveKernel(inputs, packagesFileUri, platformUri);
+  print(ComparisonNode.diff(kernelNode, analyzerNode));
+}
diff --git a/pkg/analyzer_fe_comparison/lib/src/analyzer.dart b/pkg/analyzer_fe_comparison/lib/src/analyzer.dart
new file mode 100644
index 0000000..0fa04da
--- /dev/null
+++ b/pkg/analyzer_fe_comparison/lib/src/analyzer.dart
@@ -0,0 +1,88 @@
+// 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:async';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/src/generated/source.dart' show SourceKind;
+import 'package:analyzer_fe_comparison/src/comparison_node.dart';
+
+/// Analyzes the project located at [libPath] using the analyzer, and returns a
+/// [ComparisonNode] representing it.
+Future<ComparisonNode> driveAnalyzer(String libPath) async {
+  var visitor = _AnalyzerVisitor();
+  var contextCollection = AnalysisContextCollection(includedPaths: [libPath]);
+  var contexts = contextCollection.contexts;
+  if (contexts.length != 1) {
+    throw new StateError('Expected exactly one context');
+  }
+  var context = contexts[0];
+  var session = context.currentSession;
+  var uriConverter = session.uriConverter;
+  var contextRoot = context.contextRoot;
+  var libraryNodes = <ComparisonNode>[];
+  for (var filePath in contextRoot.analyzedFiles()) {
+    var kind = await session.getSourceKind(filePath);
+    if (kind == SourceKind.LIBRARY) {
+      var importUri = uriConverter.pathToUri(filePath);
+      var libraryElement = await session.getLibraryByUri(importUri.toString());
+      var childNodes = <ComparisonNode>[];
+      if (libraryElement.name.isNotEmpty) {
+        childNodes.add(ComparisonNode('name=${libraryElement.name}'));
+      }
+      for (var compilationUnit in libraryElement.units) {
+        var unitResult =
+            await session.getResolvedAst(compilationUnit.source.fullName);
+        for (var astNode in unitResult.unit.declarations) {
+          var childNode = astNode.accept(visitor);
+          if (childNode != null) {
+            childNodes.add(childNode);
+          }
+        }
+      }
+      libraryNodes.add(ComparisonNode.sorted(importUri.toString(), childNodes));
+    }
+  }
+  return ComparisonNode.sorted('Component', libraryNodes);
+}
+
+/// Visitor for serializing the contents of an analyzer AST into
+/// ComparisonNodes.
+class _AnalyzerVisitor extends UnifyingAstVisitor<ComparisonNode> {
+  @override
+  ComparisonNode visitClassDeclaration(ClassDeclaration node) {
+    return ComparisonNode('Class ${node.name.name}');
+  }
+
+  @override
+  ComparisonNode visitEnumDeclaration(EnumDeclaration node) {
+    return ComparisonNode('Enum ${node.name.name}');
+  }
+
+  @override
+  ComparisonNode visitFunctionDeclaration(FunctionDeclaration node) {
+    // TODO(paulberry)
+    return null;
+  }
+
+  @override
+  ComparisonNode visitFunctionTypeAlias(FunctionTypeAlias node) {
+    // TODO(paulberry)
+    return null;
+  }
+
+  @override
+  ComparisonNode visitNode(AstNode node) {
+    throw new UnimplementedError('AnalyzerVisitor: ${node.runtimeType}');
+  }
+
+  @override
+  ComparisonNode visitTopLevelVariableDeclaration(
+      TopLevelVariableDeclaration node) {
+    // TODO(paulberry)
+    return null;
+  }
+}
diff --git a/pkg/analyzer_fe_comparison/lib/src/comparison_node.dart b/pkg/analyzer_fe_comparison/lib/src/comparison_node.dart
new file mode 100644
index 0000000..50e1441c
--- /dev/null
+++ b/pkg/analyzer_fe_comparison/lib/src/comparison_node.dart
@@ -0,0 +1,133 @@
+// 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:math';
+
+/// [ComparisonNode] defines a simple tree structure that can be used to compare
+/// two representations of Dart code.
+///
+/// Each node contains a textual string and a list of child nodes.
+class ComparisonNode {
+  final String text;
+  final List<ComparisonNode> children;
+
+  ComparisonNode(this.text, [List<ComparisonNode> children])
+      : children = children ?? <ComparisonNode>[];
+
+  factory ComparisonNode.sorted(
+          String text, Iterable<ComparisonNode> children) =>
+      ComparisonNode(text, sortList(children));
+
+  @override
+  bool operator ==(Object other) {
+    if (other is ComparisonNode) {
+      if (text != other.text) return false;
+      if (children.length != other.children.length) return false;
+      for (int i = 0; i < children.length; i++) {
+        if (children[i] != other.children[i]) return false;
+      }
+      return true;
+    }
+    return false;
+  }
+
+  String toString({String newline: '\n'}) {
+    var lines = ['$text'];
+    var indentedNewline = '$newline  ';
+    for (var child in children) {
+      lines.add(child.toString(newline: indentedNewline));
+    }
+    return lines.join(indentedNewline);
+  }
+
+  static ComparisonNode diff(ComparisonNode a, ComparisonNode b) {
+    String diffText;
+    if (a.text == b.text) {
+      diffText = '= ${a.text}';
+    } else {
+      diffText = 'x ${a.text} -> ${b.text}';
+    }
+    return ComparisonNode(diffText, diffLists(a.children, b.children));
+  }
+
+  static List<ComparisonNode> diffLists(
+      List<ComparisonNode> a, List<ComparisonNode> b) {
+    // Note: this is an O(n) "poor man's" diff algorithm; it produces optimal
+    // results if the incoming results are sorted by text or if there is just
+    // one contiguous hunk of differences.  Otherwise it may not find the
+    // shortest diff.  This should be sufficient for our purposes, since we are
+    // not expecting many diffs.
+
+    // We'll exclude common nodes at the beginning of both lists
+    var shorterLength = min(a.length, b.length);
+    var commonInitialNodes = 0;
+    while (commonInitialNodes < shorterLength &&
+        a[commonInitialNodes] == b[commonInitialNodes]) {
+      commonInitialNodes++;
+    }
+
+    // Fast exit if a == b
+    if (commonInitialNodes == a.length && a.length == b.length) {
+      return [];
+    }
+
+    // We'll exclude common nodes at the end of both lists (note: we don't want
+    // to overcount by re-testing the common nodes identified above)
+    var commonFinalNodes = 0;
+    while (commonInitialNodes + commonFinalNodes < shorterLength &&
+        a[a.length - commonFinalNodes - 1] ==
+            b[b.length - commonFinalNodes - 1]) {
+      commonFinalNodes++;
+    }
+
+    // Walk the remaining nodes starting at the first node that's different,
+    // matching up nodes by their text.
+    var aIndex = commonInitialNodes;
+    var bIndex = commonInitialNodes;
+    var aEnd = a.length - commonFinalNodes;
+    var bEnd = b.length - commonFinalNodes;
+    var result = <ComparisonNode>[];
+    while (aIndex < aEnd && bIndex < bEnd) {
+      var comparisonResult = a[aIndex].text.compareTo(b[bIndex].text);
+      if (comparisonResult < 0) {
+        // a[aIndex].text sorts before b[bIndex].text.  Assume that this means
+        // a[aIndex] was removed.
+        result.add(diff(a[aIndex++], ComparisonNode('(removed)')));
+      } else if (comparisonResult > 0) {
+        // b[bIndex].text sorts before a[aIndex].text.  Assume that this means
+        // b[bIndex] was added.
+        result.add(diff(ComparisonNode('(added)'), b[bIndex++]));
+      } else {
+        // a[aIndex].text matches b[bIndex].text, so diff the nodes.
+        var difference = diff(a[aIndex++], b[bIndex++]);
+        if (difference.text.startsWith('=') && difference.children.isEmpty) {
+          // Nodes are identical, so don't add anything
+        } else {
+          result.add(difference);
+        }
+      }
+    }
+
+    // Deal with any nodes left over.
+    while (aIndex < aEnd) {
+      result.add(diff(a[aIndex++], ComparisonNode('(removed)')));
+    }
+    while (bIndex < bEnd) {
+      result.add(diff(ComparisonNode('(added)'), b[bIndex++]));
+    }
+
+    // If we get here and we haven't added any nodes, something has gone wrong.
+    if (result.isEmpty) {
+      throw StateError('Diff produced empty diff for non-matching lists');
+    }
+
+    return result;
+  }
+
+  static List<ComparisonNode> sortList(Iterable<ComparisonNode> nodes) {
+    var result = nodes.toList();
+    result.sort((a, b) => a.text.compareTo(b.text));
+    return result;
+  }
+}
diff --git a/pkg/analyzer_fe_comparison/lib/src/kernel.dart b/pkg/analyzer_fe_comparison/lib/src/kernel.dart
new file mode 100644
index 0000000..9be002b
--- /dev/null
+++ b/pkg/analyzer_fe_comparison/lib/src/kernel.dart
@@ -0,0 +1,79 @@
+// 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:async';
+
+import 'package:analyzer_fe_comparison/src/comparison_node.dart';
+import 'package:front_end/src/api_prototype/compiler_options.dart';
+import 'package:front_end/src/api_prototype/kernel_generator.dart';
+import 'package:front_end/src/api_prototype/standard_file_system.dart';
+import 'package:kernel/ast.dart';
+import 'package:kernel/target/targets.dart';
+
+/// Compiles the given [inputs] to kernel using the front_end, and returns a
+/// [ComparisonNode] representing them.
+Future<ComparisonNode> driveKernel(
+    List<Uri> inputs, Uri packagesFileUri, Uri platformUri) async {
+  var targetFlags = TargetFlags(strongMode: true, syncAsync: true);
+  var target = NoneTarget(targetFlags);
+  var fileSystem = StandardFileSystem.instance;
+
+  var compilerOptions = CompilerOptions()
+    ..fileSystem = fileSystem
+    ..packagesFileUri = packagesFileUri
+    ..sdkSummary = platformUri
+    ..strongMode = true
+    ..target = target
+    ..throwOnErrorsForDebugging = true
+    ..embedSourceText = false;
+
+  var component = await kernelForComponent(inputs, compilerOptions);
+  return component.accept(new _KernelVisitor(inputs.toSet()));
+}
+
+/// Visitor for serializing a kernel representation of a program into
+/// ComparisonNodes.
+class _KernelVisitor extends TreeVisitor<ComparisonNode> {
+  final Set<Uri> _inputs;
+
+  _KernelVisitor(this._inputs);
+
+  @override
+  ComparisonNode defaultTreeNode(TreeNode node) {
+    throw new UnimplementedError('KernelVisitor: ${node.runtimeType}');
+  }
+
+  @override
+  ComparisonNode visitClass(Class class_) {
+    if (class_.isAnonymousMixin) return null;
+    var kind = class_.isEnum ? 'Enum' : 'Class';
+    // TODO(paulberry): handle fields from Class
+    return ComparisonNode('$kind ${class_.name}');
+  }
+
+  @override
+  ComparisonNode visitComponent(Component component) {
+    return ComparisonNode.sorted(
+        'Component',
+        component.libraries
+            .where((library) => _inputs.contains(library.importUri))
+            .map<ComparisonNode>((library) => library.accept(this)));
+  }
+
+  @override
+  ComparisonNode visitLibrary(Library library) {
+    var children = <ComparisonNode>[];
+    if (library.name != null) {
+      children.add(ComparisonNode('name=${library.name}'));
+    }
+    for (var class_ in library.classes) {
+      var childNode = class_.accept(this);
+      if (childNode != null) {
+        children.add(childNode);
+      }
+    }
+    // TODO(paulberry): handle more fields from Library
+    return ComparisonNode.sorted(library.importUri.toString(), children);
+  }
+}
diff --git a/pkg/analyzer_fe_comparison/pubspec.yaml b/pkg/analyzer_fe_comparison/pubspec.yaml
new file mode 100644
index 0000000..94e9893
--- /dev/null
+++ b/pkg/analyzer_fe_comparison/pubspec.yaml
@@ -0,0 +1,9 @@
+name: analyzer_fe_comparison
+version: 1.0.0
+author: Dart Team <misc@dartlang.org>
+description: Tool for comparing the behavior of the analyzer and the front end.
+homepage: https://github.com/dart-lang/sdk/tree/master/pkg/analyzer_fe_comparison
+environment:
+  sdk: '>=2.0.0-dev-48.0 <3.0.0'
+dependencies:
+  kernel: ^0.3.4
diff --git a/pkg/analyzer_fe_comparison/tool/compare_packages.dart b/pkg/analyzer_fe_comparison/tool/compare_packages.dart
new file mode 100644
index 0000000..efb0efe
--- /dev/null
+++ b/pkg/analyzer_fe_comparison/tool/compare_packages.dart
@@ -0,0 +1,33 @@
+// 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:async';
+import 'dart:io';
+
+import 'package:analyzer_fe_comparison/comparison.dart';
+import 'package:path/path.dart' as path;
+
+/// Compares the analyzer and front_end behavior when compiling a package.
+///
+/// Currently hardcoded to use the package "analyzer".
+main() async {
+  var scriptPath = Platform.script.toFilePath();
+  var sdkRepoPath =
+      path.normalize(path.join(path.dirname(scriptPath), '..', '..', '..'));
+  var buildPath = await _findBuildDir(sdkRepoPath, 'ReleaseX64');
+  var dillPath = path.join(buildPath, 'vm_platform_strong.dill');
+  var analyzerLibPath = path.join(sdkRepoPath, 'pkg', 'analyzer', 'lib');
+  var packagesFilePath = path.join(sdkRepoPath, '.packages');
+  compare(dillPath, analyzerLibPath, packagesFilePath);
+}
+
+Future<String> _findBuildDir(String sdkRepoPath, String targetName) async {
+  for (var subdirName in ['out', 'xcodebuild']) {
+    var candidatePath = path.join(sdkRepoPath, subdirName, targetName);
+    if (await new Directory(candidatePath).exists()) {
+      return candidatePath;
+    }
+  }
+  throw new StateError('Cannot find build directory');
+}