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();