Add 'show' command - displays infos as text
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47bd06b..d5b20ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,8 @@
 * Added backwards compatibility flag to the JSON codec, to make transition to
   new tools more gradual.
 
+* Added a tool to dump info files in a readable text form.
+
 * Consolidated all binary tools under a single command. Now you can access all
   tools as follows:
   ```
diff --git a/README.md b/README.md
index 7152bc8..4602546 100644
--- a/README.md
+++ b/README.md
@@ -133,6 +133,8 @@
     be JSON, backward-compatible JSON, binary, or protobuf schema (as defined in
     `info.proto`).
 
+  * [`show`][show]: a tool that dumps info files in a readable text format.
+
 Next we describe in detail how to use each of these tools.
 
 ### Code deps tool
@@ -514,3 +516,4 @@
 [function_size]: https://github.com/dart-lang/dart2js_info/blob/master/bin/function_size_analysis.dart
 [AllInfo]: http://dart-lang.github.io/dart2js_info/doc/api/dart2js_info.info/AllInfo-class.html
 [convert]: https://github.com/dart-lang/dart2js_info/blob/master/bin/convert.dart
+[show]: https://github.com/dart-lang/dart2js_info/blob/master/bin/text_print.dart
diff --git a/bin/inject_text.dart b/bin/inject_text.dart
new file mode 100644
index 0000000..8dfe157
--- /dev/null
+++ b/bin/inject_text.dart
@@ -0,0 +1,40 @@
+// Copyright (c) 2019, 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:dart2js_info/info.dart';
+
+/// Modify [info] to fill in the text of code spans.
+///
+/// By default, code spans contains the offsets but omit the text
+/// (`CodeSpan.text` is null). This function reads the output files emitted by
+/// dart2js to extract the code denoted by each span.
+void injectText(AllInfo info) {
+  // Fill the text of each code span. The binary form produced by dart2js
+  // produces code spans, but excludes the orignal text
+  info.functions.forEach((f) {
+    f.code.forEach((span) => _fillSpan(span, f.outputUnit));
+  });
+  info.fields.forEach((f) {
+    f.code.forEach((span) => _fillSpan(span, f.outputUnit));
+  });
+  info.constants.forEach((c) {
+    c.code.forEach((span) => _fillSpan(span, c.outputUnit));
+  });
+}
+
+Map<String, String> _cache = {};
+
+_getContents(OutputUnitInfo unit) => _cache.putIfAbsent(unit.filename, () {
+      var uri = Uri.base.resolve(unit.filename);
+      return new File.fromUri(uri).readAsStringSync();
+    });
+
+_fillSpan(CodeSpan span, OutputUnitInfo unit) {
+  if (span.text == null && span.start != null && span.end != 0) {
+    var contents = _getContents(unit);
+    span.text = contents.substring(span.start, span.end);
+  }
+}
diff --git a/bin/text_print.dart b/bin/text_print.dart
new file mode 100644
index 0000000..a38cd55
--- /dev/null
+++ b/bin/text_print.dart
@@ -0,0 +1,210 @@
+// Copyright (c) 2019, 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:args/command_runner.dart';
+
+import 'package:dart2js_info/info.dart';
+import 'package:dart2js_info/src/util.dart';
+import 'package:dart2js_info/src/io.dart';
+
+import 'inject_text.dart';
+import 'usage_exception.dart';
+
+/// Shows the contents of an info file as text.
+class ShowCommand extends Command<void> with PrintUsageException {
+  final String name = "show";
+  final String description = "Show a text representation of the info file.";
+
+  ShowCommand() {
+    argParser.addOption('out',
+        abbr: 'o', help: 'Output file (defauts to stdout)');
+
+    argParser.addFlag('inject-text',
+        negatable: false,
+        help: 'Whether to inject output code snippets.\n\n'
+            'By default dart2js produces code spans, but excludes the text. This\n'
+            'option can be used to embed the text directly in the output.');
+  }
+
+  void run() async {
+    if (argResults.rest.length < 1) {
+      usageException('Missing argument: <input-info>');
+    }
+
+    String filename = argResults.rest[0];
+    AllInfo info = await infoFromFile(filename);
+    if (argResults['inject-text']) injectText(info);
+
+    var buffer = new StringBuffer();
+    info.accept(new TextPrinter(buffer, argResults['inject-text']));
+    var outputPath = argResults['out'];
+    if (outputPath == null) {
+      print(buffer);
+    } else {
+      new File(outputPath).writeAsStringSync('$buffer');
+    }
+  }
+}
+
+class TextPrinter implements InfoVisitor<void> {
+  final StringBuffer buffer;
+  final bool injectText;
+
+  TextPrinter(this.buffer, this.injectText);
+
+  int _indent = 0;
+  String get _textIndent => "  " * _indent;
+  void _writeIndentation() {
+    buffer.write(_textIndent);
+  }
+
+  void _writeIndented(String s) {
+    _writeIndentation();
+    buffer.writeln(s.replaceAll('\n', '\n$_textIndent'));
+  }
+
+  void _writeBlock(String s, void f()) {
+    _writeIndented("$s");
+    _indent++;
+    f();
+    _indent--;
+  }
+
+  void visitAll(AllInfo info) {
+    _writeBlock("Summary data", () => visitProgram(info.program));
+    buffer.writeln();
+    _writeBlock("Libraries", () => info.libraries.forEach(visitLibrary));
+    // Note: classes, functions, typedefs, and fields are group;ed by library.
+
+    if (injectText) {
+      _writeBlock("Constants", () => info.constants.forEach(visitConstant));
+    } else {
+      int size = info.constants.fold(0, (n, c) => n + c.size);
+      _writeIndented("All constants: ${_size(size)}");
+    }
+    _writeBlock("Output units", () => info.outputUnits.forEach(visitOutput));
+  }
+
+  void visitProgram(ProgramInfo info) {
+    _writeIndented('main: ${longName(info.entrypoint, useLibraryUri: true)}');
+    _writeIndented('size: ${info.size}');
+    _writeIndented('dart2js-version: ${info.dart2jsVersion}');
+    var features = [];
+    if (info.noSuchMethodEnabled) features.add('no-such-method');
+    if (info.isRuntimeTypeUsed) features.add('runtime-type');
+    if (info.isFunctionApplyUsed) features.add('function-apply');
+    if (info.minified) features.add('minified');
+    if (features.isNotEmpty) {
+      _writeIndented('features: ${features.join(' ')}');
+    }
+  }
+
+  String _size(int size) {
+    if (size < 1024) return "$size b";
+    if (size < (1024 * 1024)) {
+      return "${(size / 1024).toStringAsFixed(2)} Kb ($size b)";
+    }
+    return "${(size / (1024 * 1024)).toStringAsFixed(2)} Mb ($size b)";
+  }
+
+  void visitLibrary(LibraryInfo info) {
+    _writeBlock('${info.uri}: ${_size(info.size)}', () {
+      if (info.topLevelFunctions.isNotEmpty) {
+        _writeBlock('Top-level functions',
+            () => info.topLevelFunctions.forEach(visitFunction));
+        buffer.writeln();
+      }
+      if (info.topLevelVariables.isNotEmpty) {
+        _writeBlock('Top-level variables',
+            () => info.topLevelVariables.forEach(visitField));
+        buffer.writeln();
+      }
+      if (info.classes.isNotEmpty) {
+        _writeBlock('Classes', () => info.classes.forEach(visitClass));
+      }
+      if (info.typedefs.isNotEmpty) {
+        _writeBlock("Typedefs", () => info.typedefs.forEach(visitTypedef));
+        buffer.writeln();
+      }
+      buffer.writeln();
+    });
+  }
+
+  void visitClass(ClassInfo info) {
+    _writeBlock(
+        '${info.name}: ${_size(info.size)} [${info.outputUnit.filename}]', () {
+      if (info.functions.isNotEmpty) {
+        _writeBlock('Methods:', () => info.functions.forEach(visitFunction));
+      }
+      if (info.fields.isNotEmpty) {
+        _writeBlock('Fields:', () => info.fields.forEach(visitField));
+      }
+      if (info.functions.isNotEmpty || info.fields.isNotEmpty) buffer.writeln();
+    });
+  }
+
+  void visitField(FieldInfo info) {
+    _writeBlock('${info.type} ${info.name}: ${_size(info.size)}', () {
+      _writeIndented('inferred type: ${info.inferredType}');
+      if (injectText) _writeBlock("code:", () => _writeCode(info.code));
+      if (info.closures.isNotEmpty) {
+        _writeBlock('Closures:', () => info.closures.forEach(visitClosure));
+      }
+      if (info.uses.isNotEmpty) {
+        _writeBlock('Dependencies:', () => info.uses.forEach(showDependency));
+      }
+    });
+  }
+
+  void visitFunction(FunctionInfo info) {
+    var outputUnitFile = '';
+    if (info.functionKind == FunctionInfo.TOP_LEVEL_FUNCTION_KIND) {
+      outputUnitFile = ' [${info.outputUnit.filename}]';
+    }
+    String params =
+        info.parameters.map((p) => "${p.declaredType} ${p.name}").join(', ');
+    _writeBlock(
+        '${info.returnType} ${info.name}($params): ${_size(info.size)}$outputUnitFile',
+        () {
+      String params = info.parameters.map((p) => "${p.type}").join(', ');
+      _writeIndented('declared type: ${info.type}');
+      _writeIndented(
+          'inferred type: ${info.inferredReturnType} Function($params)');
+      _writeIndented('side effects: ${info.sideEffects}');
+      if (injectText) _writeBlock("code:", () => _writeCode(info.code));
+      if (info.closures.isNotEmpty) {
+        _writeBlock('Closures:', () => info.closures.forEach(visitClosure));
+      }
+      if (info.uses.isNotEmpty) {
+        _writeBlock('Dependencies:', () => info.uses.forEach(showDependency));
+      }
+    });
+  }
+
+  void showDependency(DependencyInfo info) {
+    var mask = info.mask ?? '';
+    _writeIndented('- ${longName(info.target, useLibraryUri: true)} $mask');
+  }
+
+  void visitTypedef(TypedefInfo info) {
+    _writeIndented('${info.name}: ${info.type}');
+  }
+
+  void visitClosure(ClosureInfo info) {
+    _writeBlock('${info.name}', () => visitFunction(info.function));
+  }
+
+  void visitConstant(ConstantInfo info) {
+    _writeBlock('${_size(info.size)}:', () => _writeCode(info.code));
+  }
+
+  void _writeCode(List<CodeSpan> code) {
+    _writeIndented(code.map((c) => c.text).join('\n'));
+  }
+
+  void visitOutput(OutputUnitInfo info) {
+    _writeIndented('${info.filename}: ${_size(info.size)}');
+  }
+}
diff --git a/bin/to_json.dart b/bin/to_json.dart
index ba81c55..e7b42aa 100644
--- a/bin/to_json.dart
+++ b/bin/to_json.dart
@@ -11,6 +11,7 @@
 import 'package:dart2js_info/json_info_codec.dart';
 import 'package:dart2js_info/src/io.dart';
 
+import 'inject_text.dart';
 import 'usage_exception.dart';
 
 /// Converts a dump-info file emitted by dart2js in binary format to JSON.
@@ -50,17 +51,7 @@
     AllInfo info = await infoFromFile(filename);
 
     if (isBackwardCompatible || argResults['inject-text']) {
-      // Fill the text of each code span. The binary form produced by dart2js
-      // produces code spans, but excludes the orignal text
-      info.functions.forEach((f) {
-        f.code.forEach((span) => _fillSpan(span, f.outputUnit));
-      });
-      info.fields.forEach((f) {
-        f.code.forEach((span) => _fillSpan(span, f.outputUnit));
-      });
-      info.constants.forEach((c) {
-        c.code.forEach((span) => _fillSpan(span, c.outputUnit));
-      });
+      injectText(info);
     }
 
     var json = new AllInfoJsonCodec(isBackwardCompatible: isBackwardCompatible)
@@ -70,17 +61,3 @@
         .writeAsStringSync(const JsonEncoder.withIndent("  ").convert(json));
   }
 }
-
-Map<String, String> _cache = {};
-
-_getContents(OutputUnitInfo unit) => _cache.putIfAbsent(unit.filename, () {
-      var uri = Uri.base.resolve(unit.filename);
-      return new File.fromUri(uri).readAsStringSync();
-    });
-
-_fillSpan(CodeSpan span, OutputUnitInfo unit) {
-  if (span.text == null && span.start != null && span.end != 0) {
-    var contents = _getContents(unit);
-    span.text = contents.substring(span.start, span.end);
-  }
-}
diff --git a/bin/tools.dart b/bin/tools.dart
index 85f5281..32e9e33 100644
--- a/bin/tools.dart
+++ b/bin/tools.dart
@@ -16,6 +16,7 @@
 import 'library_size_split.dart';
 import 'live_code_size_analysis.dart';
 import 'show_inferred_types.dart';
+import 'text_print.dart';
 
 /// Entrypoint to run all dart2js_info tools.
 void main(args) {
@@ -32,6 +33,7 @@
     ..addCommand(new FunctionSizeCommand())
     ..addCommand(new LibrarySizeCommand())
     ..addCommand(new LiveCodeAnalysisCommand())
-    ..addCommand(new ShowInferredTypesCommand());
+    ..addCommand(new ShowInferredTypesCommand())
+    ..addCommand(new ShowCommand());
   commandRunner.run(args);
 }