Implement `gn edit "rename attr attr"`

Change-Id: Ia0d2208d15dbf6ff8ff9027051583b4e6a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/25461
Commit-Queue: Matt Stark <msta@google.com>
Reviewed-by: Takuto Ikuta <tikuta@google.com>
diff --git a/docs/reference.md b/docs/reference.md
index 26ab83c..7608924 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -760,6 +760,12 @@
       Example:
         gn edit "remove deps //base" //src/tools:*
 
+  rename <from_attribute> <to_attribute>
+      Renames <from_attribute> to <to_attribute>.
+
+      Example:
+        gn edit "rename srcs sources" //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
diff --git a/src/gn/edit_command.cc b/src/gn/edit_command.cc
index 8f02524..d83bdc7 100644
--- a/src/gn/edit_command.cc
+++ b/src/gn/edit_command.cc
@@ -67,6 +67,12 @@
     "      Example:\n"
     "        gn edit \"remove deps //base\" //src/tools:*\n"
     "\n"
+    "  rename <from_attribute> <to_attribute>\n"
+    "      Renames <from_attribute> to <to_attribute>.\n"
+    "\n"
+    "      Example:\n"
+    "        gn edit \"rename srcs sources\" //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"
diff --git a/src/gn/edit_command_unittest.cc b/src/gn/edit_command_unittest.cc
index e6a0e2c..e856313 100644
--- a/src/gn/edit_command_unittest.cc
+++ b/src/gn/edit_command_unittest.cc
@@ -367,6 +367,44 @@
                                        "\"deps\".")}}));
 }
 
+TEST_F(EditCommandTest, RenameSubcommand) {
+  EXPECT_SUCCESS(DoEdit("rename srcs sources",
+                        R"(
+executable("foo") {
+  # This comment should be preserved
+  srcs = [ "foo.cc" ]
+  if (is_linux) {
+    srcs += [ "linux.cc" ]
+  }
+}
+)"),
+                 Edited(R"(
+executable("foo") {
+  # This comment should be preserved
+  sources = [ "foo.cc" ]
+  if (is_linux) {
+    sources += [ "linux.cc" ]
+  }
+}
+)"));
+
+  EXPECT_SUCCESS(DoEdit("rename nonexistent new_attr", {"//:foo"},
+                        R"(
+executable("foo") {
+  sources = [ "foo.cc" ]
+}
+)"),
+                 Edited(R"(
+executable("foo") {
+  sources = [ "foo.cc" ]
+}
+)",
+                        EditState{{},
+                                  {Err(Location(),
+                                       "Target \"//:foo\" does not contain the "
+                                       "attribute \"nonexistent\".")}}));
+}
+
 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 34455f1..841f9eb 100644
--- a/src/gn/edit_subcommands.cc
+++ b/src/gn/edit_subcommands.cc
@@ -234,6 +234,25 @@
       });
 }
 
+EditCommand RenameAttributeCommand(std::string_view from_attribute,
+                                   std::string_view to_attribute) {
+  return EditTargetCommand([from_attribute = std::string(from_attribute),
+                            to_attribute = std::string(to_attribute)](
+                               BuildFile& build_file, const EditTarget& target,
+                               EditState& state) -> Err {
+    auto assignments = target.assignments(from_attribute);
+    for (auto& assignment : assignments) {
+      assignment->AsBinaryOpMut()->set_left(
+          build_file.create_identifier(to_attribute));
+    }
+    if (assignments.empty() && target.is_explicit) {
+      target.add_warning(
+          state, "does not contain the attribute \"" + from_attribute + "\".");
+    }
+    return Ok();
+  });
+}
+
 // Sets an attribute to a value.
 EditCommand SetCommand(std::string attribute, Value value) {
   return EditTargetCommand([=](BuildFile& build_file, const EditTarget& target,
@@ -295,6 +314,12 @@
     ASSIGN_OR_RETURN(std::vector<Value> values,
                      ParseValues(base::make_span(args).subspan(2)));
     return RemoveFromAttributeCommand(args[1], std::move(values));
+  } else if (args[0] == "rename") {
+    if (args.size() != 3) {
+      return Err(Location(), "Invalid rename command.",
+                 "Usage: rename <from_attribute> <to_attribute>");
+    }
+    return RenameAttributeCommand(args[1], args[2]);
   } else if (args[0] == "set") {
     if (args.size() < 3) {
       return Err(Location(),