| // Copyright 2026 The GN Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "gn/edit_subcommands.h" | |
| #include "base/containers/span.h" | |
| #include "base/strings/string_number_conversions.h" | |
| #include "gn/build_file_editor.h" | |
| #include "gn/err.h" | |
| #include "gn/location.h" | |
| #include "gn/parse_tree.h" | |
| #include "gn/value.h" | |
| namespace { | |
| // Parses a single string argument into a primitive GN Value (bool, int, or | |
| // string). | |
| Result<Value> ParseValue(std::string_view val_string) { | |
| if (val_string == "true") { | |
| return Value(nullptr, true); | |
| } | |
| if (val_string == "false") { | |
| return Value(nullptr, false); | |
| } | |
| int64_t result_int; | |
| if (base::StringToInt64(val_string, &result_int)) { | |
| return Value(nullptr, result_int); | |
| } | |
| return Value(nullptr, std::string(val_string)); | |
| } | |
| // Parses multiple string arguments into a vector of GN Values. | |
| Result<std::vector<Value>> ParseValues(base::span<const std::string> values) { | |
| std::vector<Value> list_elements; | |
| list_elements.reserve(values.size()); | |
| for (const std::string& val_str : values) { | |
| ASSIGN_OR_RETURN(Value val, ParseValue(val_str)); | |
| list_elements.push_back(std::move(val)); | |
| } | |
| return list_elements; | |
| } | |
| const TreeNode* FirstUnconditionalAssignment( | |
| const std::vector<TreeNode>& assignments) { | |
| for (const auto& assignment : assignments) { | |
| if (!assignment.is_conditional() && assignment.AsAssignment()) | |
| return &assignment; | |
| } | |
| return nullptr; | |
| } | |
| // Helper to create an EditCommand that loops over all matched targets in a | |
| // BuildFile. | |
| EditCommand EditTargetCommand( | |
| std::function<Err(BuildFile&, const EditTarget&, EditState&)> | |
| apply_to_target) { | |
| return [apply_to_target = std::move(apply_to_target)]( | |
| BuildFile& build_file, EditState& state) -> Err { | |
| for (const auto& target : build_file.targets()) { | |
| RETURN_IF_ERROR(apply_to_target(build_file, target, state)); | |
| } | |
| return Ok(); | |
| }; | |
| } | |
| bool RemoveFromTarget(const EditTarget& target, | |
| const std::string& attribute, | |
| const Value& value, | |
| EditState& state) { | |
| bool done = false; | |
| for (auto& assignment : target.assignments(attribute)) { | |
| auto matches = FindListElementInAssignment(target, assignment, value); | |
| for (const auto& match : matches) { | |
| match.RemoveSelf(state, target); | |
| } | |
| done |= !matches.empty(); | |
| } | |
| if (!done && target.is_explicit) { | |
| target.add_warning(state, "does not contain the value " + | |
| value.ToString(true) + " in attribute \"" + | |
| attribute + "\"."); | |
| } | |
| return done; | |
| } | |
| void AddToTarget(BuildFile& build_file, | |
| const EditTarget& target, | |
| const std::string& attribute, | |
| const std::vector<Value>& values) { | |
| auto assignments = target.assignments(attribute); | |
| std::vector<Value> to_add = values; | |
| // Iterate over a copy of values since we're mutating it. | |
| for (const auto& value : values) { | |
| for (auto& assignment : assignments) { | |
| auto matches = FindListElementInAssignment(target, assignment, value); | |
| for (const auto& match : matches) { | |
| if (assignment.is_conditional()) { | |
| // If it's assigned conditionally, remove it from the list first, | |
| // since we're going to assign it unconditionally. | |
| // Unlike usual we don't mark this with a comment, because this is | |
| // safe. | |
| match.RemoveSelfUnconditionally(); | |
| } else { | |
| // If it's added unconditionally, we don't need to worry about | |
| // adding it anymore. | |
| std::erase(to_add, value); | |
| } | |
| } | |
| } | |
| } | |
| if (const auto* first = FirstUnconditionalAssignment(assignments); first) { | |
| // Case A: There exists an unconditional assignment -> add values to it. | |
| ListNode* target_list = nullptr; | |
| if (auto list = FindListInAssignment(*first)) { | |
| // The expression is something like `[ "a" ]` or `foo + [ "a" ]` | |
| // In this case we just add directly to the first list "literal" we | |
| // find. | |
| target_list = *list; | |
| } else { | |
| // The expression doesn't have a list literal (eg. `foo`) | |
| // Rewrite it as `[] + foo` so we can add to the empty list. | |
| auto* op = first->AsAssignment(); | |
| auto empty_list_val = | |
| build_file.to_node(Value(nullptr, std::vector<Value>{})); | |
| target_list = empty_list_val->AsListMut(); | |
| auto plus_node = std::make_unique<BinaryOpNode>(); | |
| plus_node->set_op(Token(build_file.location(), Token::PLUS, "+")); | |
| plus_node->set_left(std::move(empty_list_val)); | |
| plus_node->set_right(op->take_right()); | |
| op->set_right(std::move(plus_node)); | |
| } | |
| for (const auto& value : to_add) { | |
| target_list->append_item(build_file.to_node(value)); | |
| } | |
| } else if (!assignments.empty()) { | |
| // Case B: attr is only defined conditionally -> add attr = [value] at the | |
| // start of the block, change all other assignments to "+=". | |
| for (auto& assignment : assignments) { | |
| if (auto* op = assignment->AsBinaryOpMut()) { | |
| if (op->op().type() == Token::EQUAL) { | |
| op->set_op(Token(op->op().location(), Token::PLUS_EQUALS, "+=")); | |
| } | |
| } | |
| } | |
| target.block->statements().insert( | |
| target.block->statements().begin(), | |
| build_file.create_assignment( | |
| attribute, | |
| build_file.to_node(Value(nullptr, std::vector<Value>(to_add))))); | |
| } else { | |
| // Case C: attr is not defined -> add attr = [value] at the end of the | |
| // block. | |
| target.block->append_statement(build_file.create_assignment( | |
| attribute, | |
| build_file.to_node(Value(nullptr, std::vector<Value>(to_add))))); | |
| } | |
| } | |
| EditCommand AddToAttributeCommand(std::string attribute, | |
| std::vector<Value> values) { | |
| return EditTargetCommand( | |
| [attribute = std::move(attribute), values = std::move(values)]( | |
| BuildFile& build_file, const EditTarget& target, | |
| EditState& state) -> Err { | |
| AddToTarget(build_file, target, attribute, values); | |
| return Ok(); | |
| }); | |
| } | |
| EditCommand DeleteCommand() { | |
| return EditTargetCommand([](BuildFile& build_file, const EditTarget& target, | |
| EditState& state) -> Err { | |
| target.node.RemoveSelf(state, target); | |
| return Ok(); | |
| }); | |
| } | |
| EditCommand MoveCommand(std::string from_attribute, | |
| std::string to_attribute, | |
| std::vector<Value> values) { | |
| return EditTargetCommand([from_attribute = std::move(from_attribute), | |
| to_attribute = std::move(to_attribute), | |
| values = std::move(values)]( | |
| BuildFile& build_file, const EditTarget& target, | |
| EditState& state) -> Err { | |
| std::vector<Value> moved_values; | |
| for (const auto& value : values) { | |
| if (RemoveFromTarget(target, from_attribute, value, state)) { | |
| moved_values.push_back(value); | |
| } | |
| } | |
| if (!moved_values.empty()) { | |
| AddToTarget(build_file, target, to_attribute, moved_values); | |
| } | |
| return Ok(); | |
| }); | |
| } | |
| using LocationProvider = | |
| std::function<Result<TreeNode::NodeLocation>(BuildFile&)>; | |
| EditCommand NewCommand(std::string rule_kind, | |
| LocationProvider location_provider) { | |
| return [rule_kind = std::move(rule_kind), | |
| location_provider = std::move(location_provider)]( | |
| BuildFile& build_file, EditState& state) -> Err { | |
| ASSIGN_OR_RETURN(auto rule_names, | |
| build_file.label_matcher().explicit_target_names()); | |
| DCHECK(!rule_names.empty()); | |
| ASSIGN_OR_RETURN(auto loc, location_provider(build_file)); | |
| auto& [container, it] = loc; | |
| for (const auto& rule_name : rule_names) { | |
| if (auto target = build_file.find_target(rule_name); | |
| target && !target->node.is_conditional()) { | |
| return Err(Location(), "Target \"" + rule_name + | |
| "\" already exists in " + | |
| build_file.source_file().value() + "."); | |
| } | |
| // Reassign and advance the iterator returned by insert() to ensure valid | |
| // iterators across vector reallocations and preserve insertion order | |
| // when adding multiple targets. | |
| it = container.insert( | |
| it, build_file.create_target(rule_kind, rule_name, | |
| build_file.create_block())); | |
| ++it; | |
| } | |
| 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(); | |
| }); | |
| } | |
| EditCommand RemoveFromAttributeCommand(std::string attribute, | |
| std::vector<Value> values) { | |
| return EditTargetCommand( | |
| [attribute = std::move(attribute), values = std::move(values)]( | |
| BuildFile& build_file, const EditTarget& target, | |
| EditState& state) -> Err { | |
| for (const auto& value : values) { | |
| RemoveFromTarget(target, attribute, value, state); | |
| } | |
| return Ok(); | |
| }); | |
| } | |
| 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, | |
| 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); | |
| } | |
| } | |
| 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))); | |
| } | |
| return Ok(); | |
| }); | |
| } | |
| } // namespace | |
| Result<EditCommand> ParseCommand(std::vector<std::string> args) { | |
| if (args.empty()) { | |
| return Err(Location(), "Empty command."); | |
| } | |
| if (args[0] == "add") { | |
| if (args.size() < 3) { | |
| return Err(Location(), "Invalid add command.", | |
| "Usage: add <attribute> <value(s)>"); | |
| } | |
| ASSIGN_OR_RETURN(std::vector<Value> values, | |
| ParseValues(base::make_span(args).subspan(2))); | |
| return AddToAttributeCommand(args[1], std::move(values)); | |
| } else if (args[0] == "delete") { | |
| if (args.size() != 1) { | |
| return Err(Location(), "Invalid delete command.", "Usage: delete"); | |
| } | |
| return DeleteCommand(); | |
| } else if (args[0] == "move") { | |
| if (args.size() < 4) { | |
| return Err(Location(), "Invalid move command.", | |
| "Usage: move <from_attribute> <to_attribute> <value(s)>"); | |
| } | |
| ASSIGN_OR_RETURN(std::vector<Value> values, | |
| ParseValues(base::make_span(args).subspan(3))); | |
| return MoveCommand(args[1], args[2], std::move(values)); | |
| } else if (args[0] == "new") { | |
| if (args.size() == 2) { | |
| return NewCommand( | |
| args[1], [](BuildFile& build_file) -> Result<TreeNode::NodeLocation> { | |
| auto* root = build_file.root()->AsBlockMut(); | |
| return std::make_pair(std::ref(root->statements()), | |
| root->statements().end()); | |
| }); | |
| } else if (args.size() == 4 && | |
| (args[2] == "before" || args[2] == "after")) { | |
| return NewCommand( | |
| args[1], | |
| [rel = args[3], after = args[2] == "after"]( | |
| BuildFile& build_file) -> Result<TreeNode::NodeLocation> { | |
| auto target = build_file.find_target(rel); | |
| if (!target) { | |
| return Err(Location(), "Target \"" + rel + "\" not found in " + | |
| build_file.source_file().value() + | |
| "."); | |
| } | |
| auto [container, it] = target->node.node_location(); | |
| if (after) | |
| it++; | |
| return std::make_pair(std::ref(container), it); | |
| }); | |
| } else { | |
| return Err(Location(), "Invalid new command.", | |
| "Usage: new <rule_kind> [(before|after) " | |
| "<relative_rule_name>]"); | |
| } | |
| } else if (args[0] == "remove") { | |
| if (args.size() < 2) { | |
| return Err(Location(), "Invalid remove command.", | |
| "Usage: remove <attribute> [<value(s)>]"); | |
| } else if (args.size() == 2) { | |
| return RemoveAttributeCommand(args[1]); | |
| } | |
| 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(), | |
| "Invalid set command: missing attribute or value.\n" | |
| "Usage: set <attribute> <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)); | |
| } else { | |
| return Err(Location(), | |
| "Unknown edit command: " + std::string(args[0]) + | |
| "\n" | |
| "See `gn help edit` for list of supported commands."); | |
| } | |
| } |