pub deps --json (#2896)

diff --git a/lib/src/command/deps.dart b/lib/src/command/deps.dart
index 98e68a3..d061b96 100644
--- a/lib/src/command/deps.dart
+++ b/lib/src/command/deps.dart
@@ -3,8 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:collection';
-
-import 'package:path/path.dart' as p;
+import 'dart:convert';
 
 import '../ascii_tree.dart' as tree;
 import '../command.dart';
@@ -53,6 +52,10 @@
     argParser.addFlag('executables',
         negatable: false, help: 'List all available executables.');
 
+    argParser.addFlag('json',
+        negatable: false,
+        help: 'Output dependency information in a json format.');
+
     argParser.addOption('directory',
         abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir');
   }
@@ -64,26 +67,98 @@
 
     _buffer = StringBuffer();
 
-    if (argResults['executables']) {
-      _outputExecutables();
-    } else {
-      for (var sdk in sdks.values) {
-        if (!sdk.isAvailable) continue;
-        _buffer.writeln("${log.bold('${sdk.name} SDK')} ${sdk.version}");
+    if (argResults['json']) {
+      if (argResults.wasParsed('dev')) {
+        usageException(
+            'Cannot combine --json and --dev.\nThe json output contains the dependency type in the output.');
       }
+      if (argResults.wasParsed('executables')) {
+        usageException(
+            'Cannot combine --json and --executables.\nThe json output always lists available executables.');
+      }
+      if (argResults.wasParsed('style')) {
+        usageException('Cannot combine --json and --style.');
+      }
+      final visited = <String>[];
+      final toVisit = [entrypoint.root.name];
+      final packagesJson = <dynamic>[];
+      while (toVisit.isNotEmpty) {
+        final current = toVisit.removeLast();
+        if (visited.contains(current)) continue;
+        visited.add(current);
+        final currentPackage = entrypoint.packageGraph.packages[current];
+        final next = (current == entrypoint.root.name
+                ? entrypoint.root.immediateDependencies
+                : currentPackage.dependencies)
+            .keys
+            .toList();
+        final dependencyType = entrypoint.root.dependencyType(current);
+        final kind = currentPackage == entrypoint.root
+            ? 'root'
+            : (dependencyType == DependencyType.direct
+                ? 'direct'
+                : (dependencyType == DependencyType.dev
+                    ? 'dev'
+                    : 'transitive'));
+        final source =
+            entrypoint.packageGraph.lockFile.packages[current]?.source?.name ??
+                'root';
+        packagesJson.add({
+          'name': current,
+          'version': currentPackage.version.toString(),
+          'kind': kind,
+          'source': source,
+          'dependencies': next
+        });
+        toVisit.addAll(next);
+      }
+      var executables = [
+        for (final package in [
+          entrypoint.root,
+          ...entrypoint.root.immediateDependencies.keys
+              .map((name) => entrypoint.packageGraph.packages[name])
+        ])
+          ...package.executableNames.map((name) => package == entrypoint.root
+              ? ':$name'
+              : (package.name == name ? name : '${package.name}:$name'))
+      ];
 
-      _buffer.writeln(_labelPackage(entrypoint.root));
+      _buffer.writeln(
+        JsonEncoder.withIndent('  ').convert(
+          {
+            'root': entrypoint.root.name,
+            'packages': packagesJson,
+            'sdks': [
+              for (var sdk in sdks.values)
+                if (sdk.version != null)
+                  {'name': sdk.name, 'version': sdk.version.toString()}
+            ],
+            'executables': executables
+          },
+        ),
+      );
+    } else {
+      if (argResults['executables']) {
+        _outputExecutables();
+      } else {
+        for (var sdk in sdks.values) {
+          if (!sdk.isAvailable) continue;
+          _buffer.writeln("${log.bold('${sdk.name} SDK')} ${sdk.version}");
+        }
 
-      switch (argResults['style']) {
-        case 'compact':
-          _outputCompact();
-          break;
-        case 'list':
-          _outputList();
-          break;
-        case 'tree':
-          _outputTree();
-          break;
+        _buffer.writeln(_labelPackage(entrypoint.root));
+
+        switch (argResults['style']) {
+          case 'compact':
+            _outputCompact();
+            break;
+          case 'list':
+            _outputList();
+            break;
+          case 'tree':
+            _outputTree();
+            break;
+        }
       }
     }
 
@@ -268,34 +343,13 @@
     ];
 
     for (var package in packages) {
-      var executables = _getExecutablesFor(package);
+      var executables = package.executableNames;
       if (executables.isNotEmpty) {
         _buffer.writeln(_formatExecutables(package.name, executables.toList()));
       }
     }
   }
 
-  /// Returns `true` if [path] looks like a Dart entrypoint.
-  bool _isDartExecutable(String path) {
-    try {
-      var unit = analysisContextManager.parse(path);
-      return isEntrypoint(unit);
-    } on AnalyzerErrorGroup {
-      return false;
-    }
-  }
-
-  /// Lists all Dart files in the `bin` directory of the [package].
-  ///
-  /// Returns file names without extensions.
-  Iterable<String> _getExecutablesFor(Package package) {
-    var packagePath = p.normalize(p.absolute(package.dir));
-    analysisContextManager.createContextsForDirectory(packagePath);
-    return package.executablePaths
-        .where((e) => _isDartExecutable(p.absolute(package.dir, e)))
-        .map(p.basenameWithoutExtension);
-  }
-
   /// Returns formatted string that lists [executables] for the [packageName].
   /// Examples:
   ///
diff --git a/lib/src/package.dart b/lib/src/package.dart
index 55ce4fe..88a2909 100644
--- a/lib/src/package.dart
+++ b/lib/src/package.dart
@@ -84,6 +84,9 @@
         .toList();
   }
 
+  List<String> get executableNames =>
+      executablePaths.map(p.basenameWithoutExtension).toList();
+
   /// Returns the path to the README file at the root of the entrypoint, or null
   /// if no README file is found.
   ///
diff --git a/test/deps/executables_test.dart b/test/deps/executables_test.dart
index 933189f..88b1a88 100644
--- a/test/deps/executables_test.dart
+++ b/test/deps/executables_test.dart
@@ -2,226 +2,163 @@
 // 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:pub/src/ascii_tree.dart' as tree;
+import 'package:pub/src/io.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
+import '../golden_file.dart';
 import '../test_pub.dart';
 
 const _validMain = 'main() {}';
+const _invalidMain = 'main() {';
+
+Future<void> variations(String name) async {
+  final buffer = StringBuffer();
+  buffer.writeln(
+      tree.fromFiles(listDir(d.sandbox, recursive: true), baseDir: d.sandbox));
+
+  await pubGet();
+  await runPubIntoBuffer(['deps', '--executables'], buffer);
+  await runPubIntoBuffer(['deps', '--executables', '--dev'], buffer);
+  // The json ouput also lists the exectuables.
+  await runPubIntoBuffer(['deps', '--json'], buffer);
+  // The easiest way to update the golden files is to delete them and rerun the
+  // test.
+  expectMatchesGoldenFile(buffer.toString(), 'test/deps/goldens/$name.txt');
+}
 
 void main() {
-  Future<void> Function() _testExecutablesOutput(output, {bool dev = true}) =>
-      () async {
-        await pubGet();
-        await runPub(
-            args: ['deps', '--executables', if (dev) '--dev' else '--no-dev'],
-            output: output);
-      };
-
-  Future<void> Function() _testAllDepsOutput(output) =>
-      _testExecutablesOutput(output);
-  Future<void> Function() _testNonDevDepsOutput(output) =>
-      _testExecutablesOutput(output, dev: false);
-
-  group('lists nothing when no executables found', () {
-    setUp(() async {
-      await d.dir(appPath, [d.appPubspec()]).create();
-    });
-
-    test('all dependencies', _testAllDepsOutput('\n'));
-    test('non-dev dependencies', _testNonDevDepsOutput('\n'));
+  test('skips non-Dart executables', () async {
+    await d.dir(appPath, [
+      d.appPubspec(),
+      d.dir('bin', [d.file('foo.py'), d.file('bar.sh')])
+    ]).create();
+    await variations('non_dart_executables');
   });
 
-  group('skips non-Dart executables', () {
-    setUp(() async {
-      await d.dir(appPath, [
-        d.appPubspec(),
-        d.dir('bin', [d.file('foo.py'), d.file('bar.sh')])
-      ]).create();
-    });
-
-    test('all dependencies', _testAllDepsOutput('\n'));
-    test('non-dev dependencies', _testNonDevDepsOutput('\n'));
+  test('lists Dart executables, even without entrypoints', () async {
+    await d.dir(appPath, [
+      d.appPubspec(),
+      d.dir(
+        'bin',
+        [d.file('foo.dart', _validMain), d.file('bar.dart', _invalidMain)],
+      )
+    ]).create();
+    await variations('dart_executables');
   });
 
-  group('skips Dart executables which are not parsable', () {
-    setUp(() async {
-      await d.dir(appPath, [
-        d.appPubspec(),
-        d.dir('bin', [d.file('foo.dart', 'main() {')])
-      ]).create();
-    });
-
-    test('all dependencies', _testAllDepsOutput('\n'));
-    test('non-dev dependencies', _testNonDevDepsOutput('\n'));
+  test('skips executables in sub directories', () async {
+    await d.dir(appPath, [
+      d.appPubspec(),
+      d.dir('bin', [
+        d.file('foo.dart', _validMain),
+        d.dir('sub', [d.file('bar.dart', _validMain)])
+      ])
+    ]).create();
+    await variations('nothing_in_sub_drectories');
   });
 
-  group('skips Dart executables without entrypoints', () {
-    setUp(() async {
-      await d.dir(appPath, [
-        d.appPubspec(),
-        d.dir(
-            'bin', [d.file('foo.dart'), d.file('bar.dart', 'main(x, y, z) {}')])
-      ]).create();
-    });
+  test('lists executables from a dependency', () async {
+    await d.dir('foo', [
+      d.libPubspec('foo', '1.0.0'),
+      d.dir('bin', [d.file('bar.dart', _validMain)])
+    ]).create();
 
-    test('all dependencies', _testAllDepsOutput('\n'));
-    test('non-dev dependencies', _testNonDevDepsOutput('\n'));
+    await d.dir(appPath, [
+      d.appPubspec({
+        'foo': {'path': '../foo'}
+      })
+    ]).create();
+
+    await variations('from_dependency');
   });
 
-  group('lists valid Dart executables with entrypoints', () {
-    setUp(() async {
-      await d.dir(appPath, [
-        d.appPubspec(),
-        d.dir('bin',
-            [d.file('foo.dart', _validMain), d.file('bar.dart', _validMain)])
-      ]).create();
-    });
+  test('lists executables only from immediate dependencies', () async {
+    await d.dir(appPath, [
+      d.appPubspec({
+        'foo': {'path': '../foo'}
+      })
+    ]).create();
 
-    test('all dependencies', _testAllDepsOutput('myapp: bar, foo'));
-    test('non-dev dependencies', _testNonDevDepsOutput('myapp: bar, foo'));
+    await d.dir('foo', [
+      d.libPubspec('foo', '1.0.0', deps: {
+        'baz': {'path': '../baz'}
+      }),
+      d.dir('bin', [d.file('bar.dart', _validMain)])
+    ]).create();
+
+    await d.dir('baz', [
+      d.libPubspec('baz', '1.0.0'),
+      d.dir('bin', [d.file('qux.dart', _validMain)])
+    ]).create();
+
+    await variations('only_immediate');
   });
 
-  group('skips executables in sub directories', () {
-    setUp(() async {
-      await d.dir(appPath, [
-        d.appPubspec(),
-        d.dir('bin', [
-          d.file('foo.dart', _validMain),
-          d.dir('sub', [d.file('bar.dart', _validMain)])
-        ])
-      ]).create();
-    });
+  test('applies formatting before printing executables', () async {
+    await d.dir(appPath, [
+      d.appPubspec({
+        'foo': {'path': '../foo'},
+        'bar': {'path': '../bar'}
+      }),
+      d.dir('bin', [d.file('myapp.dart', _validMain)])
+    ]).create();
 
-    test('all dependencies', _testAllDepsOutput('myapp:foo'));
-    test('non-dev dependencies', _testNonDevDepsOutput('myapp:foo'));
+    await d.dir('foo', [
+      d.libPubspec('foo', '1.0.0'),
+      d.dir('bin',
+          [d.file('baz.dart', _validMain), d.file('foo.dart', _validMain)])
+    ]).create();
+
+    await d.dir('bar', [
+      d.libPubspec('bar', '1.0.0'),
+      d.dir('bin', [d.file('qux.dart', _validMain)])
+    ]).create();
+
+    await variations('formatting');
   });
 
-  group('lists executables from a dependency', () {
-    setUp(() async {
-      await d.dir('foo', [
-        d.libPubspec('foo', '1.0.0'),
-        d.dir('bin', [d.file('bar.dart', _validMain)])
-      ]).create();
+  test('dev dependencies', () async {
+    await d.dir('foo', [
+      d.libPubspec('foo', '1.0.0'),
+      d.dir('bin', [d.file('bar.dart', _validMain)])
+    ]).create();
 
-      await d.dir(appPath, [
-        d.appPubspec({
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'myapp',
+        'dev_dependencies': {
           'foo': {'path': '../foo'}
-        })
-      ]).create();
-    });
-
-    test('all dependencies', _testAllDepsOutput('foo:bar'));
-    test('non-dev dependencies', _testNonDevDepsOutput('foo:bar'));
+        }
+      })
+    ]).create();
+    await variations('dev_dependencies');
   });
 
-  group('lists executables only from immediate dependencies', () {
-    setUp(() async {
-      await d.dir(appPath, [
-        d.appPubspec({
-          'foo': {'path': '../foo'}
-        })
-      ]).create();
+  test('overriden dependencies executables', () async {
+    await d.dir('foo-1.0', [
+      d.libPubspec('foo', '1.0.0'),
+      d.dir('bin', [d.file('bar.dart', _validMain)])
+    ]).create();
 
-      await d.dir('foo', [
-        d.libPubspec('foo', '1.0.0', deps: {
-          'baz': {'path': '../baz'}
-        }),
-        d.dir('bin', [d.file('bar.dart', _validMain)])
-      ]).create();
+    await d.dir('foo-2.0', [
+      d.libPubspec('foo', '2.0.0'),
+      d.dir('bin',
+          [d.file('bar.dart', _validMain), d.file('baz.dart', _validMain)])
+    ]).create();
 
-      await d.dir('baz', [
-        d.libPubspec('baz', '1.0.0'),
-        d.dir('bin', [d.file('qux.dart', _validMain)])
-      ]).create();
-    });
-
-    test('all dependencies', _testAllDepsOutput('foo:bar'));
-    test('non-dev dependencies', _testNonDevDepsOutput('foo:bar'));
-  });
-
-  group('applies formatting before printing executables', () {
-    setUp(() async {
-      await d.dir(appPath, [
-        d.appPubspec({
-          'foo': {'path': '../foo'},
-          'bar': {'path': '../bar'}
-        }),
-        d.dir('bin', [d.file('myapp.dart', _validMain)])
-      ]).create();
-
-      await d.dir('foo', [
-        d.libPubspec('foo', '1.0.0'),
-        d.dir('bin',
-            [d.file('baz.dart', _validMain), d.file('foo.dart', _validMain)])
-      ]).create();
-
-      await d.dir('bar', [
-        d.libPubspec('bar', '1.0.0'),
-        d.dir('bin', [d.file('qux.dart', _validMain)])
-      ]).create();
-    });
-
-    test('all dependencies', _testAllDepsOutput('''
-        myapp
-        foo: foo, baz
-        bar:qux'''));
-    test('non-dev dependencies', _testNonDevDepsOutput('''
-        myapp
-        foo: foo, baz
-        bar:qux'''));
-  });
-
-  group('dev dependencies', () {
-    setUp(() async {
-      await d.dir('foo', [
-        d.libPubspec('foo', '1.0.0'),
-        d.dir('bin', [d.file('bar.dart', _validMain)])
-      ]).create();
-
-      await d.dir(appPath, [
-        d.pubspec({
-          'name': 'myapp',
-          'dev_dependencies': {
-            'foo': {'path': '../foo'}
-          }
-        })
-      ]).create();
-    });
-
-    test('are listed if --dev flag is set', _testAllDepsOutput('foo:bar'));
-    test('are skipped if --no-dev flag is set', _testNonDevDepsOutput('\n'));
-  });
-
-  group('overriden dependencies executables', () {
-    setUp(() async {
-      await d.dir('foo-1.0', [
-        d.libPubspec('foo', '1.0.0'),
-        d.dir('bin', [d.file('bar.dart', _validMain)])
-      ]).create();
-
-      await d.dir('foo-2.0', [
-        d.libPubspec('foo', '2.0.0'),
-        d.dir('bin',
-            [d.file('bar.dart', _validMain), d.file('baz.dart', _validMain)])
-      ]).create();
-
-      await d.dir(appPath, [
-        d.pubspec({
-          'name': 'myapp',
-          'dependencies': {
-            'foo': {'path': '../foo-1.0'}
-          },
-          'dependency_overrides': {
-            'foo': {'path': '../foo-2.0'}
-          }
-        })
-      ]).create();
-    });
-
-    test(
-        'are listed if --dev flag is set', _testAllDepsOutput('foo: bar, baz'));
-    test('are listed if --no-dev flag is set',
-        _testNonDevDepsOutput('foo: bar, baz'));
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'myapp',
+        'dependencies': {
+          'foo': {'path': '../foo-1.0'}
+        },
+        'dependency_overrides': {
+          'foo': {'path': '../foo-2.0'}
+        }
+      })
+    ]).create();
+    await variations('overrides');
   });
 }
diff --git a/test/deps/goldens/dart_executables.txt b/test/deps/goldens/dart_executables.txt
new file mode 100644
index 0000000..9284ceb
--- /dev/null
+++ b/test/deps/goldens/dart_executables.txt
@@ -0,0 +1,36 @@
+'-- myapp
+    |-- bin
+    |   |-- bar.dart
+    |   '-- foo.dart
+    '-- pubspec.yaml
+
+$ pub deps --executables
+myapp: bar, foo
+
+$ pub deps --executables --dev
+myapp: bar, foo
+
+$ pub deps --json
+{
+  "root": "myapp",
+  "packages": [
+    {
+      "name": "myapp",
+      "version": "0.0.0",
+      "kind": "root",
+      "source": "root",
+      "dependencies": []
+    }
+  ],
+  "sdks": [
+    {
+      "name": "Dart",
+      "version": "0.1.2+3"
+    }
+  ],
+  "executables": [
+    ":bar",
+    ":foo"
+  ]
+}
+
diff --git a/test/deps/goldens/dev_dependencies.txt b/test/deps/goldens/dev_dependencies.txt
new file mode 100644
index 0000000..3fb677c
--- /dev/null
+++ b/test/deps/goldens/dev_dependencies.txt
@@ -0,0 +1,45 @@
+|-- foo
+|   |-- bin
+|   |   '-- bar.dart
+|   '-- pubspec.yaml
+'-- myapp
+    '-- pubspec.yaml
+
+$ pub deps --executables
+foo:bar
+
+$ pub deps --executables --dev
+foo:bar
+
+$ pub deps --json
+{
+  "root": "myapp",
+  "packages": [
+    {
+      "name": "myapp",
+      "version": "0.0.0",
+      "kind": "root",
+      "source": "root",
+      "dependencies": [
+        "foo"
+      ]
+    },
+    {
+      "name": "foo",
+      "version": "1.0.0",
+      "kind": "dev",
+      "source": "path",
+      "dependencies": []
+    }
+  ],
+  "sdks": [
+    {
+      "name": "Dart",
+      "version": "0.1.2+3"
+    }
+  ],
+  "executables": [
+    "foo:bar"
+  ]
+}
+
diff --git a/test/deps/goldens/formatting.txt b/test/deps/goldens/formatting.txt
new file mode 100644
index 0000000..ff5aec9
--- /dev/null
+++ b/test/deps/goldens/formatting.txt
@@ -0,0 +1,67 @@
+|-- bar
+|   |-- bin
+|   |   '-- qux.dart
+|   '-- pubspec.yaml
+|-- foo
+|   |-- bin
+|   |   |-- baz.dart
+|   |   '-- foo.dart
+|   '-- pubspec.yaml
+'-- myapp
+    |-- bin
+    |   '-- myapp.dart
+    '-- pubspec.yaml
+
+$ pub deps --executables
+myapp
+foo: foo, baz
+bar:qux
+
+$ pub deps --executables --dev
+myapp
+foo: foo, baz
+bar:qux
+
+$ pub deps --json
+{
+  "root": "myapp",
+  "packages": [
+    {
+      "name": "myapp",
+      "version": "0.0.0",
+      "kind": "root",
+      "source": "root",
+      "dependencies": [
+        "foo",
+        "bar"
+      ]
+    },
+    {
+      "name": "bar",
+      "version": "1.0.0",
+      "kind": "direct",
+      "source": "path",
+      "dependencies": []
+    },
+    {
+      "name": "foo",
+      "version": "1.0.0",
+      "kind": "direct",
+      "source": "path",
+      "dependencies": []
+    }
+  ],
+  "sdks": [
+    {
+      "name": "Dart",
+      "version": "0.1.2+3"
+    }
+  ],
+  "executables": [
+    ":myapp",
+    "foo:baz",
+    "foo",
+    "bar:qux"
+  ]
+}
+
diff --git a/test/deps/goldens/from_dependency.txt b/test/deps/goldens/from_dependency.txt
new file mode 100644
index 0000000..30836e5
--- /dev/null
+++ b/test/deps/goldens/from_dependency.txt
@@ -0,0 +1,45 @@
+|-- foo
+|   |-- bin
+|   |   '-- bar.dart
+|   '-- pubspec.yaml
+'-- myapp
+    '-- pubspec.yaml
+
+$ pub deps --executables
+foo:bar
+
+$ pub deps --executables --dev
+foo:bar
+
+$ pub deps --json
+{
+  "root": "myapp",
+  "packages": [
+    {
+      "name": "myapp",
+      "version": "0.0.0",
+      "kind": "root",
+      "source": "root",
+      "dependencies": [
+        "foo"
+      ]
+    },
+    {
+      "name": "foo",
+      "version": "1.0.0",
+      "kind": "direct",
+      "source": "path",
+      "dependencies": []
+    }
+  ],
+  "sdks": [
+    {
+      "name": "Dart",
+      "version": "0.1.2+3"
+    }
+  ],
+  "executables": [
+    "foo:bar"
+  ]
+}
+
diff --git a/test/deps/goldens/non_dart_executables.txt b/test/deps/goldens/non_dart_executables.txt
new file mode 100644
index 0000000..646e21e
--- /dev/null
+++ b/test/deps/goldens/non_dart_executables.txt
@@ -0,0 +1,31 @@
+'-- myapp
+    |-- bin
+    |   |-- bar.sh
+    |   '-- foo.py
+    '-- pubspec.yaml
+
+$ pub deps --executables
+
+$ pub deps --executables --dev
+
+$ pub deps --json
+{
+  "root": "myapp",
+  "packages": [
+    {
+      "name": "myapp",
+      "version": "0.0.0",
+      "kind": "root",
+      "source": "root",
+      "dependencies": []
+    }
+  ],
+  "sdks": [
+    {
+      "name": "Dart",
+      "version": "0.1.2+3"
+    }
+  ],
+  "executables": []
+}
+
diff --git a/test/deps/goldens/nothing_in_sub_drectories.txt b/test/deps/goldens/nothing_in_sub_drectories.txt
new file mode 100644
index 0000000..db3c77c
--- /dev/null
+++ b/test/deps/goldens/nothing_in_sub_drectories.txt
@@ -0,0 +1,36 @@
+'-- myapp
+    |-- bin
+    |   |-- foo.dart
+    |   '-- sub
+    |       '-- bar.dart
+    '-- pubspec.yaml
+
+$ pub deps --executables
+myapp:foo
+
+$ pub deps --executables --dev
+myapp:foo
+
+$ pub deps --json
+{
+  "root": "myapp",
+  "packages": [
+    {
+      "name": "myapp",
+      "version": "0.0.0",
+      "kind": "root",
+      "source": "root",
+      "dependencies": []
+    }
+  ],
+  "sdks": [
+    {
+      "name": "Dart",
+      "version": "0.1.2+3"
+    }
+  ],
+  "executables": [
+    ":foo"
+  ]
+}
+
diff --git a/test/deps/goldens/only_immediate.txt b/test/deps/goldens/only_immediate.txt
new file mode 100644
index 0000000..5e42925
--- /dev/null
+++ b/test/deps/goldens/only_immediate.txt
@@ -0,0 +1,58 @@
+|-- baz
+|   |-- bin
+|   |   '-- qux.dart
+|   '-- pubspec.yaml
+|-- foo
+|   |-- bin
+|   |   '-- bar.dart
+|   '-- pubspec.yaml
+'-- myapp
+    '-- pubspec.yaml
+
+$ pub deps --executables
+foo:bar
+
+$ pub deps --executables --dev
+foo:bar
+
+$ pub deps --json
+{
+  "root": "myapp",
+  "packages": [
+    {
+      "name": "myapp",
+      "version": "0.0.0",
+      "kind": "root",
+      "source": "root",
+      "dependencies": [
+        "foo"
+      ]
+    },
+    {
+      "name": "foo",
+      "version": "1.0.0",
+      "kind": "direct",
+      "source": "path",
+      "dependencies": [
+        "baz"
+      ]
+    },
+    {
+      "name": "baz",
+      "version": "1.0.0",
+      "kind": "transitive",
+      "source": "path",
+      "dependencies": []
+    }
+  ],
+  "sdks": [
+    {
+      "name": "Dart",
+      "version": "0.1.2+3"
+    }
+  ],
+  "executables": [
+    "foo:bar"
+  ]
+}
+
diff --git a/test/deps/goldens/overrides.txt b/test/deps/goldens/overrides.txt
new file mode 100644
index 0000000..ab76e79
--- /dev/null
+++ b/test/deps/goldens/overrides.txt
@@ -0,0 +1,51 @@
+|-- foo-1.0
+|   |-- bin
+|   |   '-- bar.dart
+|   '-- pubspec.yaml
+|-- foo-2.0
+|   |-- bin
+|   |   |-- bar.dart
+|   |   '-- baz.dart
+|   '-- pubspec.yaml
+'-- myapp
+    '-- pubspec.yaml
+
+$ pub deps --executables
+foo: bar, baz
+
+$ pub deps --executables --dev
+foo: bar, baz
+
+$ pub deps --json
+{
+  "root": "myapp",
+  "packages": [
+    {
+      "name": "myapp",
+      "version": "0.0.0",
+      "kind": "root",
+      "source": "root",
+      "dependencies": [
+        "foo"
+      ]
+    },
+    {
+      "name": "foo",
+      "version": "2.0.0",
+      "kind": "direct",
+      "source": "path",
+      "dependencies": []
+    }
+  ],
+  "sdks": [
+    {
+      "name": "Dart",
+      "version": "0.1.2+3"
+    }
+  ],
+  "executables": [
+    "foo:bar",
+    "foo:baz"
+  ]
+}
+
diff --git a/test/deps_test.dart b/test/deps_test.dart
index d1ab44d..207e8e7 100644
--- a/test/deps_test.dart
+++ b/test/deps_test.dart
@@ -130,6 +130,128 @@
                       '-- myapp...
           ''');
     });
+    test('in json form', () async {
+      await pubGet();
+      await runPub(args: ['deps', '--json'], output: '''
+{
+  "root": "myapp",
+  "packages": [
+    {
+      "name": "myapp",
+      "version": "0.0.0",
+      "kind": "root",
+      "source": "root",
+      "dependencies": [
+        "normal",
+        "overridden",
+        "from_path",
+        "unittest",
+        "override_only"
+      ]
+    },
+    {
+      "name": "override_only",
+      "version": "1.2.3",
+      "kind": "transitive",
+      "source": "hosted",
+      "dependencies": []
+    },
+    {
+      "name": "unittest",
+      "version": "1.2.3",
+      "kind": "dev",
+      "source": "hosted",
+      "dependencies": [
+        "shared",
+        "dev_only"
+      ]
+    },
+    {
+      "name": "dev_only",
+      "version": "1.2.3",
+      "kind": "transitive",
+      "source": "hosted",
+      "dependencies": []
+    },
+    {
+      "name": "shared",
+      "version": "1.2.3",
+      "kind": "transitive",
+      "source": "hosted",
+      "dependencies": [
+        "other"
+      ]
+    },
+    {
+      "name": "other",
+      "version": "1.0.0",
+      "kind": "transitive",
+      "source": "hosted",
+      "dependencies": [
+        "myapp"
+      ]
+    },
+    {
+      "name": "from_path",
+      "version": "1.2.3",
+      "kind": "direct",
+      "source": "path",
+      "dependencies": []
+    },
+    {
+      "name": "overridden",
+      "version": "2.0.0",
+      "kind": "direct",
+      "source": "hosted",
+      "dependencies": []
+    },
+    {
+      "name": "normal",
+      "version": "1.2.3",
+      "kind": "direct",
+      "source": "hosted",
+      "dependencies": [
+        "transitive",
+        "circular_a"
+      ]
+    },
+    {
+      "name": "circular_a",
+      "version": "1.2.3",
+      "kind": "transitive",
+      "source": "hosted",
+      "dependencies": [
+        "circular_b"
+      ]
+    },
+    {
+      "name": "circular_b",
+      "version": "1.2.3",
+      "kind": "transitive",
+      "source": "hosted",
+      "dependencies": [
+        "circular_a"
+      ]
+    },
+    {
+      "name": "transitive",
+      "version": "1.2.3",
+      "kind": "transitive",
+      "source": "hosted",
+      "dependencies": [
+        "shared"
+      ]
+    }
+  ],
+  "sdks": [
+    {
+      "name": "Dart",
+      "version": "0.1.2+3"
+    }
+  ],
+  "executables": []
+}''');
+    });
 
     test('with the Flutter SDK, if applicable', () async {
       await pubGet();