Pub add/remove now remove dependencies key if it makes them empty (#2639)

* Pub add/remove now remove dependencies key if it makes them empty

* Modified tests
diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index 73b8833..1ba0410 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:pub_semver/pub_semver.dart';
+import 'package:yaml/yaml.dart';
 
 import '../command.dart';
 import '../entrypoint.dart';
@@ -391,11 +392,18 @@
 
     /// Remove the package from dev_dependencies if we are adding it to
     /// dependencies. Refer to [_addPackageToPubspec] for additional discussion.
-    if (!isDevelopment &&
-        yamlEditor.parseAt(['dev_dependencies', package.name],
-                orElse: () => null) !=
-            null) {
-      yamlEditor.remove(['dev_dependencies', package.name]);
+    if (!isDevelopment) {
+      final devDependenciesNode =
+          yamlEditor.parseAt(['dev_dependencies'], orElse: () => null);
+
+      if (devDependenciesNode is YamlMap &&
+          devDependenciesNode.containsKey(package.name)) {
+        if (devDependenciesNode.length == 1) {
+          yamlEditor.remove(['dev_dependencies']);
+        } else {
+          yamlEditor.remove(['dev_dependencies', package.name]);
+        }
+      }
     }
 
     /// Windows line endings are already handled by [yamlEditor]
diff --git a/lib/src/command/remove.dart b/lib/src/command/remove.dart
index 94091c7..16aefbc 100644
--- a/lib/src/command/remove.dart
+++ b/lib/src/command/remove.dart
@@ -2,6 +2,8 @@
 // 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:yaml/yaml.dart';
+
 import '../command.dart';
 import '../entrypoint.dart';
 import '../io.dart';
@@ -97,9 +99,17 @@
       /// There may be packages where the dependency is declared both in
       /// dependencies and dev_dependencies.
       for (final dependencyKey in ['dependencies', 'dev_dependencies']) {
-        if (yamlEditor.parseAt([dependencyKey, package], orElse: () => null) !=
-            null) {
-          yamlEditor.remove([dependencyKey, package]);
+        final dependenciesNode =
+            yamlEditor.parseAt([dependencyKey], orElse: () => null);
+
+        if (dependenciesNode is YamlMap &&
+            dependenciesNode.containsKey(package)) {
+          if (dependenciesNode.length == 1) {
+            yamlEditor.remove([dependencyKey]);
+          } else {
+            yamlEditor.remove([dependencyKey, package]);
+          }
+
           found = true;
         }
       }
diff --git a/test/add/common/add_test.dart b/test/add/common/add_test.dart
index 3b9f3af..cca9fd9 100644
--- a/test/add/common/add_test.dart
+++ b/test/add/common/add_test.dart
@@ -50,6 +50,37 @@
       await d.appDir({'foo': '1.2.3'}).validate();
     });
 
+    test(
+        'does not remove empty dev_dependencies while adding to normal dependencies',
+        () async {
+      await servePackages((builder) {
+        builder.serve('foo', '1.2.3');
+        builder.serve('foo', '1.2.2');
+      });
+
+      await d.dir(appPath, [
+        YamlDescriptor('pubspec.yaml', '''
+          name: myapp
+          dependencies: 
+
+          dev_dependencies:
+        ''')
+      ]).create();
+
+      await pubAdd(args: ['foo:1.2.3']);
+
+      await d.cacheDir({'foo': '1.2.3'}).validate();
+      await d.appPackagesFile({'foo': '1.2.3'}).validate();
+
+      await d.dir(appPath, [
+        d.pubspec({
+          'name': 'myapp',
+          'dependencies': {'foo': '1.2.3'},
+          'dev_dependencies': null
+        })
+      ]).validate();
+    });
+
     test('dry run does not actually add the package or modify the pubspec',
         () async {
       await servePackages((builder) => builder.serve('foo', '1.2.3'));
@@ -150,11 +181,13 @@
       });
 
       await d.dir(appPath, [
-        d.pubspec({
-          'name': 'myapp',
-          'dependencies': {},
-          'dev_dependencies': {'foo': '1.2.2'}
-        })
+        YamlDescriptor('pubspec.yaml', '''
+name: myapp
+dependencies: 
+
+dev_dependencies:
+  foo: 1.2.2
+''')
       ]).create();
 
       await pubAdd(
@@ -165,11 +198,11 @@
 
       await d.cacheDir({'foo': '1.2.3'}).validate();
       await d.appPackagesFile({'foo': '1.2.3'}).validate();
+
       await d.dir(appPath, [
         d.pubspec({
           'name': 'myapp',
-          'dependencies': {'foo': '1.2.3'},
-          'dev_dependencies': {}
+          'dependencies': {'foo': '1.2.3'}
         })
       ]).validate();
     });
@@ -758,17 +791,9 @@
 
     await pubAdd(args: ['bar']);
 
-    final finalPubspec = YamlDescriptor('pubspec.yaml', '''
-      name: myapp
-      dependencies: 
-        bar: ^1.0.0''');
-    await d.dir(appPath, [finalPubspec]).validate();
-    final fullPath = p.join(d.sandbox, appPath, 'pubspec.yaml');
-
-    expect(File(fullPath).existsSync(), true);
-
-    final contents = File(fullPath).readAsStringSync();
-    expect(contents, await finalPubspec.read());
+    await d.dir(appPath, [
+      d.appPubspec({'bar': '^1.0.0'})
+    ]).validate();
   });
 
   test('preserves comments', () async {
@@ -790,20 +815,19 @@
 
     await pubAdd(args: ['bar']);
 
-    final finalPubspec = YamlDescriptor('pubspec.yaml', '''
-      name: myapp
-      dependencies: # comment A
-          # comment B
-          bar: ^1.0.0
-          foo: 1.0.0 # comment C
-        # comment D
-    ''');
-    await d.dir(appPath, [finalPubspec]).validate();
+    await d.appDir({'bar': '^1.0.0', 'foo': '1.0.0'}).validate();
     final fullPath = p.join(d.sandbox, appPath, 'pubspec.yaml');
 
     expect(File(fullPath).existsSync(), true);
 
     final contents = File(fullPath).readAsStringSync();
-    expect(contents, await finalPubspec.read());
+    expect(
+        contents,
+        allOf([
+          contains('# comment A'),
+          contains('# comment B'),
+          contains('# comment C'),
+          contains('# comment D')
+        ]));
   });
 }
diff --git a/test/remove/remove_test.dart b/test/remove/remove_test.dart
index 50dba5d..3691405 100644
--- a/test/remove/remove_test.dart
+++ b/test/remove/remove_test.dart
@@ -22,7 +22,39 @@
 
     await d.cacheDir({}).validate();
     await d.appPackagesFile({}).validate();
-    await d.appDir({}).validate();
+    await d.appDir().validate();
+  });
+
+  test('removing a package from dependencies does not affect dev_dependencies',
+      () async {
+    await servePackages((builder) {
+      builder.serve('foo', '1.2.3');
+      builder.serve('foo', '1.2.2');
+      builder.serve('bar', '2.0.0');
+    });
+
+    await d.dir(appPath, [
+      YamlDescriptor('pubspec.yaml', '''
+name: myapp
+dependencies: 
+  foo: 1.2.3
+
+dev_dependencies:
+  bar: 2.0.0
+''')
+    ]).create();
+
+    await pubRemove(args: ['foo']);
+
+    await d.cacheDir({'bar': '2.0.0'}).validate();
+    await d.appPackagesFile({'bar': '2.0.0'}).validate();
+
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'myapp',
+        'dev_dependencies': {'bar': '2.0.0'}
+      })
+    ]).validate();
   });
 
   test('dry-run does not actually remove dependency', () async {
@@ -80,7 +112,7 @@
     await d.appPackagesFile({}).validate();
 
     await d.dir(appPath, [
-      d.pubspec({'name': 'myapp', 'dev_dependencies': {}})
+      d.pubspec({'name': 'myapp'})
     ]).validate();
   });
 
@@ -111,7 +143,6 @@
       d.pubspec({
         'name': 'myapp',
         'dependencies': {'jfj': '0.2.1'},
-        'dev_dependencies': {}
       })
     ]).validate();
   });
@@ -199,6 +230,7 @@
 
     await pubRemove(args: ['bar']);
 
+    await d.appDir({'foo': '1.0.0'}).validate();
     final fullPath = p.join(d.sandbox, appPath, 'pubspec.yaml');
     expect(File(fullPath).existsSync(), true);
     final contents = File(fullPath).readAsStringSync();