Implement `gn edit delete` and `gn edit remove attr`

Change-Id: Ib41ef3fa55e622cecd6fea3e748305ef6a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/25400
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: Matt Stark <msta@google.com>
diff --git a/docs/reference.md b/docs/reference.md
index 8053e94..671d3d0 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -728,19 +728,27 @@
 
 #### **Commands**:
 ```
+  delete
+      Deletes the matched targets entirely.
+
+      Example:
+        gn edit "delete" //src/tools:old_target
+
+  remove <attribute>
+      Removes <attribute> entirely.
+
+      Example:
+        gn edit "remove testonly" //src/tools:*
+
   set <attribute>[:list] <value(s)>
       Sets or overwrites the target's <attribute> to <value(s)>.
       If multiple values are provided, or if the ":list" suffix is
       appended to the attribute, <value(s)> is interpreted as a list.
-```
 
-#### **Examples**:
-```
-  gn edit "set testonly true" //src/tools:*
-      Sets 'testonly' to 'true' for all targets in
-      `//src/tools/BUILD.gn`.
-  gn edit "set srcs:list foo.cc foo.h" //:foo
-      Sets 'srcs' to '[ "foo.cc", "foo.h" ]' for //:foo.
+      Examples:
+        gn edit "set testonly true" //src/tools:*
+        gn edit "set srcs:list foo.cc" //:foo
+        gn edit "set deps :bar :baz" //:foo
 ```
 ### <a name="cmd_format"></a>**gn format [\--dump-tree] [\--format-width=WIDTH] (\--stdin | &lt;list of build_files...&gt;)**&nbsp;[Back to Top](#gn-reference)
 
diff --git a/src/gn/build_file_editor.cc b/src/gn/build_file_editor.cc
index 01a346d..e784edc 100644
--- a/src/gn/build_file_editor.cc
+++ b/src/gn/build_file_editor.cc
@@ -89,7 +89,8 @@
 }  // namespace
 
 bool TreeNode::is_conditional() const {
-  for (auto it = stack_.rbegin(); it != stack_.rend(); ++it) {
+  DCHECK(!stack_.empty()) << "stack should never be empty";
+  for (auto it = stack_.rbegin() + 1; it != stack_.rend(); ++it) {
     if ((*it)->AsCondition()) {
       return true;
     }
diff --git a/src/gn/edit_command.cc b/src/gn/edit_command.cc
index b8095ff..2ca0d3d 100644
--- a/src/gn/edit_command.cc
+++ b/src/gn/edit_command.cc
@@ -35,17 +35,27 @@
     "  in your build files instructing you what to do.\n"
     "\n"
     "Commands:\n"
+    "  delete\n"
+    "      Deletes the matched targets entirely.\n"
+    "\n"
+    "      Example:\n"
+    "        gn edit \"delete\" //src/tools:old_target\n"
+    "\n"
+    "  remove <attribute>\n"
+    "      Removes <attribute> entirely.\n"
+    "\n"
+    "      Example:\n"
+    "        gn edit \"remove testonly\" //src/tools:*\n"
+    "\n"
     "  set <attribute>[:list] <value(s)>\n"
     "      Sets or overwrites the target's <attribute> to <value(s)>.\n"
     "      If multiple values are provided, or if the \":list\" suffix is\n"
     "      appended to the attribute, <value(s)> is interpreted as a list.\n"
     "\n"
-    "Examples:\n"
-    "  gn edit \"set testonly true\" //src/tools:*\n"
-    "      Sets 'testonly' to 'true' for all targets in\n"
-    "      `//src/tools/BUILD.gn`.\n"
-    "  gn edit \"set srcs:list foo.cc foo.h\" //:foo\n"
-    "      Sets 'srcs' to '[ \"foo.cc\", \"foo.h\" ]' for //:foo.\n";
+    "      Examples:\n"
+    "        gn edit \"set testonly true\" //src/tools:*\n"
+    "        gn edit \"set srcs:list foo.cc\" //:foo\n"
+    "        gn edit \"set deps :bar :baz\" //:foo\n";
 
 Result<std::pair<std::vector<SourceFile>, EditState>> RunEditImpl(
     const std::vector<std::string>& args,
diff --git a/src/gn/edit_command_unittest.cc b/src/gn/edit_command_unittest.cc
index 1961e56..cea3a60 100644
--- a/src/gn/edit_command_unittest.cc
+++ b/src/gn/edit_command_unittest.cc
@@ -125,6 +125,84 @@
                  "Target(s) not found: //:nonexistent");
 }
 
+TEST_F(EditCommandTest, DeleteSubcommand) {
+  EXPECT_SUCCESS(DoEdit("delete", {"//:bar"},
+                        R"(
+executable("foo") {
+  sources = [ "foo.cc" ]
+}
+executable("bar") {
+  sources = [ "bar.cc" ]
+}
+)"),
+                 Edited(R"(
+executable("foo") {
+  sources = [ "foo.cc" ]
+}
+)"));
+
+  EXPECT_SUCCESS(DoEdit("delete", {"//:bar"},
+                        R"(
+if (is_win) {
+  executable("bar") {
+    sources = [ "bar.cc" ]
+  }
+} else {
+  executable("bar") {
+    sources = [ "bar.cc" ]
+  }
+}
+)"),
+                 Edited(R"(
+if (is_win) {
+  # TODO(gn edit: delete):
+  # This would normally be deleted but is conditional.
+  # Manual intervention is required to decide whether it should actually be deleted.
+  executable("bar") {
+    sources = [ "bar.cc" ]
+  }
+} else {
+  # TODO(gn edit: delete):
+  # This would normally be deleted but is conditional.
+  # Manual intervention is required to decide whether it should actually be deleted.
+  executable("bar") {
+    sources = [ "bar.cc" ]
+  }
+}
+)",
+                        EditState({Label(SourceDir("//"), "bar")})));
+}
+
+TEST_F(EditCommandTest, RemoveAttributeSubcommand) {
+  EXPECT_SUCCESS(DoEdit("remove testonly",
+                        R"(
+executable("foo") {
+  sources = [ "foo.cc" ]
+  testonly = true
+}
+)"),
+                 Edited(R"(
+executable("foo") {
+  sources = [ "foo.cc" ]
+}
+)"));
+
+  EXPECT_SUCCESS(
+      DoEdit("remove nonexistent_attribute", {"//:foo"},
+             R"(
+executable("foo") {
+}
+)"),
+      Edited(R"(
+executable("foo") {
+}
+)",
+             EditState{{},
+                       {Err(Location(),
+                            "Target \"//:foo\" does not contain the "
+                            "attribute \"nonexistent_attribute\".")}}));
+}
+
 TEST_F(EditCommandTest, SetSubcommand) {
   // New bool attribute
   EXPECT_SUCCESS(DoEdit("set testonly true",
diff --git a/src/gn/edit_subcommands.cc b/src/gn/edit_subcommands.cc
index 243cff5..31c18f5 100644
--- a/src/gn/edit_subcommands.cc
+++ b/src/gn/edit_subcommands.cc
@@ -65,6 +65,30 @@
   };
 }
 
+EditCommand DeleteCommand() {
+  return EditTargetCommand([](BuildFile& build_file, const EditTarget& target,
+                              EditState& state) -> Err {
+    target.node.RemoveSelf(state, target);
+    return Ok();
+  });
+}
+
+EditCommand RemoveAttributeCommand(std::string attribute) {
+  return EditTargetCommand([attribute = std::move(attribute)](
+                               BuildFile& build_file, const EditTarget& target,
+                               EditState& state) -> Err {
+    auto assignments = target.assignments(attribute);
+    for (auto& assignment : assignments) {
+      assignment.RemoveSelf(state, target);
+    }
+    if (assignments.empty() && target.is_explicit) {
+      target.add_warning(
+          state, "does not contain the attribute \"" + attribute + "\".");
+    }
+    return Ok();
+  });
+}
+
 // Sets an attribute to a value.
 EditCommand SetCommand(std::string attribute, Value value) {
   return EditTargetCommand([=](BuildFile& build_file, const EditTarget& target,
@@ -95,7 +119,18 @@
     return Err(Location(), "Empty command.");
   }
 
-  if (args[0] == "set") {
+  if (args[0] == "delete") {
+    if (args.size() != 1) {
+      return Err(Location(), "Invalid delete command.", "Usage: delete");
+    }
+    return DeleteCommand();
+  } else if (args[0] == "remove") {
+    if (args.size() != 2) {
+      return Err(Location(), "Invalid remove command.",
+                 "Usage: remove <attribute>");
+    }
+    return RemoveAttributeCommand(args[1]);
+  } else if (args[0] == "set") {
     if (args.size() < 3) {
       return Err(Location(),
                  "Invalid set command: missing attribute or value.\n"
@@ -121,10 +156,10 @@
     }
 
     return SetCommand(std::string(attribute), std::move(val));
+  } else {
+    return Err(Location(),
+               "Unknown edit command: " + std::string(args[0]) +
+                   "\n"
+                   "See `gn help edit` for list of supported commands.");
   }
-
-  return Err(Location(),
-             "Unknown edit command: " + std::string(args[0]) +
-                 "\n"
-                 "See `gn help edit` for list of supported commands.");
 }