Support setting expressions in `gn edit`

Previously the expressions would be converted to a string, but now they
can be evaluated as an expression

Change-Id: Id583c8071b6e5c4b46a68a7806b3c3a56a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/26060
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 1e7743f..e77824a 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -778,15 +778,27 @@
       Example:
         gn edit "rename srcs sources" //src/tools:*
 
-  set <attribute>[:list] <value(s)>
+  set <attribute>[:list|:expr] <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.
+      If the ":expr" suffix is appended to the attribute, <value(s)>
+      is parsed as a raw GN expression (e.g. variable, list of variables,
+      or expression).
 
       Examples:
         gn edit "set testonly true" //src/tools:*
+          => testonly = true
+        gn edit "set output_name my_tool" //src/tools:*
+          => output_name = "my_tool"
+        gn edit "set deps:expr default_deps" //:foo
+          => deps = default_deps
         gn edit "set srcs:list foo.cc" //:foo
+          => srcs = [ "foo.cc" ]
         gn edit "set deps :bar :baz" //:foo
+          => deps = [ ":bar", ":baz" ]
+        gn edit "set deps:expr a + b" //:foo
+          => deps = a + b
 
   shard [sharded_target_type] [group_type]
       Splits the target's sources into fine-grained shard targets,
diff --git a/src/gn/build_file_editor.cc b/src/gn/build_file_editor.cc
index 6535c43..7800024 100644
--- a/src/gn/build_file_editor.cc
+++ b/src/gn/build_file_editor.cc
@@ -476,20 +476,28 @@
   return std::nullopt;
 }
 
-std::unique_ptr<ParseNode> BuildFile::to_node(const Value& value) {
+Result<std::unique_ptr<ParseNode>> BuildFile::parse_expression(
+    std::string_view expr_string) {
   auto file = std::make_unique<InputFile>(SourceFile("//dummy"));
-  file->SetContents(value.ToString(true));
+  file->SetContents(std::string(expr_string));
 
   Err err;
   std::vector<Token> tokens = Tokenizer::Tokenize(file.get(), &err);
+  RETURN_IF_ERROR(err);
   for (auto& token : tokens) {
     token.set_location(this->location());
   }
   auto parsed = Parser::ParseExpression(tokens, &err);
+  RETURN_IF_ERROR(err);
   extra_files_.push_back(std::move(file));
+  return std::move(parsed);
+}
+
+std::unique_ptr<ParseNode> BuildFile::to_node(const Value& value) {
+  auto parsed = parse_expression(value.ToString(true));
   // value.ToString() must return something parsable as input to GN.
-  DCHECK(!err.has_error());
-  return parsed;
+  DCHECK(!parsed.has_error());
+  return std::move(*parsed);
 }
 
 std::unique_ptr<IdentifierNode> BuildFile::create_identifier(
diff --git a/src/gn/build_file_editor.h b/src/gn/build_file_editor.h
index 70b27f1..90c5839 100644
--- a/src/gn/build_file_editor.h
+++ b/src/gn/build_file_editor.h
@@ -229,6 +229,10 @@
   // Creates a node to insert into the graph.
   std::unique_ptr<ParseNode> to_node(const Value& value);
 
+  // Parses a raw GN expression string into a ParseNode.
+  Result<std::unique_ptr<ParseNode>> parse_expression(
+      std::string_view expr_string);
+
   // Creates a node for an identifier.
   std::unique_ptr<IdentifierNode> create_identifier(std::string_view value);
   // Creates a node for `a = b`
diff --git a/src/gn/edit_command.cc b/src/gn/edit_command.cc
index 192bd49..62d49a3 100644
--- a/src/gn/edit_command.cc
+++ b/src/gn/edit_command.cc
@@ -83,15 +83,28 @@
     "      Example:\n"
     "        gn edit \"rename srcs sources\" //src/tools:*\n"
     "\n"
-    "  set <attribute>[:list] <value(s)>\n"
+    "  set <attribute>[:list|:expr] <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"
+    "      If the \":expr\" suffix is appended to the attribute, <value(s)>\n"
+    "      is parsed as a raw GN expression (e.g. variable, list of "
+    "variables,\n"
+    "      or expression).\n"
     "\n"
     "      Examples:\n"
     "        gn edit \"set testonly true\" //src/tools:*\n"
+    "          => testonly = true\n"
+    "        gn edit \"set output_name my_tool\" //src/tools:*\n"
+    "          => output_name = \"my_tool\"\n"
+    "        gn edit \"set deps:expr default_deps\" //:foo\n"
+    "          => deps = default_deps\n"
     "        gn edit \"set srcs:list foo.cc\" //:foo\n"
+    "          => srcs = [ \"foo.cc\" ]\n"
     "        gn edit \"set deps :bar :baz\" //:foo\n"
+    "          => deps = [ \":bar\", \":baz\" ]\n"
+    "        gn edit \"set deps:expr a + b\" //:foo\n"
+    "          => deps = a + b\n"
     "\n"
     "  shard [sharded_target_type] [group_type]\n"
     "      Splits the target's sources into fine-grained shard targets,\n"
@@ -126,6 +139,16 @@
       return Err(Location(), "Unclosed quote in command string.");
     }
     command_tokens.push_back(std::move(token));
+
+    // When calling set on an expression, we don't want to use std::quoted,
+    // we want to load the string raw.
+    if (command_tokens.size() == 2 && command_tokens[0] == "set" &&
+        command_tokens[1].ends_with(":expr")) {
+      if (!(ss >> std::ws).eof()) {
+        command_tokens.emplace_back(args[0].substr(ss.tellg()));
+      }
+      break;
+    }
   }
   if (command_tokens.empty()) {
     return Err(Location(), "Empty command string.");
diff --git a/src/gn/edit_command_unittest.cc b/src/gn/edit_command_unittest.cc
index 7614da4..dd426f7 100644
--- a/src/gn/edit_command_unittest.cc
+++ b/src/gn/edit_command_unittest.cc
@@ -623,6 +623,25 @@
 }
 )",
                      EditState({Label(SourceDir("//"), "foo")})));
+
+  // Custom Expressions
+  EXPECT_SUCCESS(DoEdit("set str:expr default + \"a b\"",
+                        R"(
+executable("foo") {
+}
+)"),
+                 Edited(R"(
+executable("foo") {
+  str = default + "a b"
+}
+)"));
+
+  EXPECT_FAILURE(DoEdit("set deps:unknown a b",
+                        R"(
+executable("foo") {
+}
+)"),
+                 "Unknown type: :unknown");
 }
 
 TEST_F(EditCommandTest, ShardSubcommand) {
diff --git a/src/gn/edit_subcommands.cc b/src/gn/edit_subcommands.cc
index 595feda..dbcc051 100644
--- a/src/gn/edit_subcommands.cc
+++ b/src/gn/edit_subcommands.cc
@@ -12,6 +12,7 @@
 
 #include "base/containers/span.h"
 #include "base/strings/string_number_conversions.h"
+#include "base/strings/string_util.h"
 #include "gn/build_file_editor.h"
 #include "gn/err.h"
 #include "gn/location.h"
@@ -49,6 +50,47 @@
   return list_elements;
 }
 
+std::pair<std::string_view, std::optional<std::string_view>> SplitAttrType(
+    std::string_view arg) {
+  size_t colon_pos = arg.find(':');
+  if (colon_pos == std::string_view::npos) {
+    return {arg, std::nullopt};
+  }
+  return {arg.substr(0, colon_pos), arg.substr(colon_pos + 1)};
+}
+
+using ParseNodeGenerator =
+    std::function<Result<std::unique_ptr<ParseNode>>(BuildFile& build_file)>;
+
+Result<ParseNodeGenerator> CreateParseNodeGenerator(
+    std::optional<std::string_view> kind,
+    base::span<const std::string> values) {
+  CHECK(!values.empty());
+
+  if (kind == "expr") {
+    std::string expr_string = base::JoinString(
+        std::vector<std::string_view>(values.begin(), values.end()), " ");
+    return [expr_string = std::move(expr_string)](BuildFile& build_file) {
+      return build_file.parse_expression(expr_string);
+    };
+  } else if (kind == "list" || (!kind && values.size() > 1)) {
+    ASSIGN_OR_RETURN(std::vector<Value> out, ParseValues(values));
+    return [out = std::move(out)](
+               BuildFile& build_file) -> Result<std::unique_ptr<ParseNode>> {
+      return build_file.to_node(Value(nullptr, std::vector<Value>(out)));
+    };
+  } else if (!kind.has_value()) {
+    ASSIGN_OR_RETURN(Value val, ParseValue(values[0]));
+    return [val = std::move(val)](
+               BuildFile& build_file) -> Result<std::unique_ptr<ParseNode>> {
+      return build_file.to_node(val);
+    };
+  }
+
+  return Err(Location(), "Unknown type: :" + std::string(*kind),
+             "Supported types are :list and :expr.");
+}
+
 const TreeNode* FirstUnconditionalAssignment(
     const std::vector<TreeNode>& assignments) {
   for (const auto& assignment : assignments) {
@@ -182,6 +224,7 @@
         return Ok();
       });
 }
+
 EditCommand DeleteCommand() {
   return EditTargetCommand([](BuildFile& build_file, const EditTarget& target,
                               EditState& state) -> Err {
@@ -294,27 +337,30 @@
   });
 }
 
-// Sets an attribute to a value.
-EditCommand SetCommand(std::string attribute, Value value) {
-  return EditTargetCommand([=](BuildFile& build_file, const EditTarget& target,
-                               EditState& state) -> Err {
-    auto assignments = target.assignments(attribute);
-    const auto* first = FirstUnconditionalAssignment(assignments);
-    for (const auto& assignment : assignments) {
-      if (&assignment != first) {
-        assignment.RemoveSelf(state, target);
-      }
-    }
+// Sets an attribute to an expression
+EditCommand SetCommand(std::string attribute, ParseNodeGenerator generator) {
+  return EditTargetCommand(
+      [attribute = std::move(attribute), generator = std::move(generator)](
+          BuildFile& build_file, const EditTarget& target,
+          EditState& state) -> Err {
+        ASSIGN_OR_RETURN(auto node, generator(build_file));
+        auto assignments = target.assignments(attribute);
+        const auto* first = FirstUnconditionalAssignment(assignments);
+        for (const auto& assignment : assignments) {
+          if (&assignment != first) {
+            assignment.RemoveSelf(state, target);
+          }
+        }
 
-    if (first) {
-      (*first)->AsBinaryOpMut()->set_right(build_file.to_node(value));
-    } else {
-      target.block->append_statement(
-          build_file.create_assignment(attribute, build_file.to_node(value)));
-    }
+        if (first) {
+          (*first)->AsBinaryOpMut()->set_right(std::move(node));
+        } else {
+          target.block->append_statement(
+              build_file.create_assignment(attribute, std::move(node)));
+        }
 
-    return Ok();
-  });
+        return Ok();
+      });
 }
 
 Result<std::string> GetShardName(const LocationRange& location,
@@ -519,28 +565,14 @@
     if (args.size() < 3) {
       return Err(Location(),
                  "Invalid set command: missing attribute or value.\n"
-                 "Usage: set <attribute> <value...>");
+                 "Usage: set <attribute>[:type] <value...>");
     }
 
-    std::string_view attribute = args[1];
-    bool force_list = false;
-    constexpr std::string_view kListSuffix = ":list";
-    if (attribute.ends_with(kListSuffix)) {
-      attribute.remove_suffix(kListSuffix.size());
-      force_list = true;
-    }
-
-    auto value_args = base::make_span(args).subspan(2);
-    Value val;
-    if (value_args.size() > 1 || force_list) {
-      ASSIGN_OR_RETURN(std::vector<Value> list_elements,
-                       ParseValues(value_args));
-      val = Value(nullptr, std::move(list_elements));
-    } else {
-      ASSIGN_OR_RETURN(val, ParseValue(value_args[0]));
-    }
-
-    return SetCommand(std::string(attribute), std::move(val));
+    auto [attr, kind] = SplitAttrType(args[1]);
+    ASSIGN_OR_RETURN(
+        auto generator,
+        CreateParseNodeGenerator(kind, base::make_span(args).subspan(2)));
+    return SetCommand(std::string(attr), std::move(generator));
   } else if (args[0] == "shard") {
     if (args.size() == 1) {
       return ShardCommand();