blob: 595feda39d08035af4aa986da19adff696527ea6 [file]
// 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 <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>
#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();
});
}
Result<std::string> GetShardName(const LocationRange& location,
std::string_view path) {
if (path.empty() || path.starts_with("/")) {
return Err(
location,
"Cannot shard target with non-relative source path: " +
std::string(path),
"Determining a shard name is not supported for non-relative paths.");
}
if (path.starts_with("./")) {
path.remove_prefix(2);
}
size_t last_slash = path.rfind('/');
size_t last_dot = path.rfind('.');
if (last_dot != std::string_view::npos &&
(last_slash == std::string_view::npos || last_dot > last_slash)) {
path = path.substr(0, last_dot);
}
std::string shard_name;
shard_name.reserve(path.size());
for (char c : path) {
if (c == '/' || c == '\\' || c == '.' || c == '-') {
shard_name.push_back('_');
} else {
shard_name.push_back(c);
}
}
return shard_name;
}
EditCommand ShardCommand(
std::optional<std::string> shard_target_type = std::nullopt,
std::string group_type = "group") {
return EditTargetCommand([shard_target_type, group_type](
BuildFile& build_file, const EditTarget& target,
EditState& state) -> Err {
std::set<std::string> shards;
for (auto assign : target.assignments({"sources", "public"})) {
for (const auto& item : FindAllListElements(assign)) {
if (auto lit = AsStringLiteral(item.node()); lit) {
ASSIGN_OR_RETURN(std::string shard_name,
GetShardName(item->GetRange(), *lit));
shards.insert(std::move(shard_name));
}
}
}
if (shards.size() <= 1) {
target.add_warning(state, "does not need to be sharded.");
return Ok();
}
for (auto assign : target.assignments({"public_deps", "deps"})) {
assign.RemoveSelfUnconditionally();
}
std::vector<Value> target_names;
auto [container, it] = target.node.node_location();
// Ensure we insert the shard after the group.
++it;
for (const auto& shard : shards) {
// Note: If a target "foo" contains both "foo.cc" and "foo_foo.cc", both
// would map to "foo_foo". This is a rare edge case that is not worth
// complex disambiguation logic.
std::string target_name = (shard == target.label.name())
? (target.label.name() + "_" + shard)
: shard;
target_names.push_back(Value(nullptr, ":" + target_name));
state.needs_fix_deps.insert(Label(target.label.dir(), target_name));
auto node = target.node.node()->Clone();
auto* func = node->AsFunctionCallMut();
if (shard_target_type) {
func->set_function(Token(func->function().location(), Token::IDENTIFIER,
*shard_target_type));
}
it = container.insert(it, std::move(node));
++it;
func->args()->contents()[0] =
build_file.to_node(Value(nullptr, target_name));
for (auto assign :
TreeNode({func->block()}).assignments({"sources", "public"})) {
for (const auto& item : FindAllListElements(assign)) {
if (auto lit = AsStringLiteral(item.node()); lit) {
auto res = GetShardName(item->GetRange(), *lit);
DCHECK(!res.has_error()) << "should have failed above";
if (*res != shard) {
item.RemoveSelfUnconditionally();
}
}
}
}
}
auto* orig_func = target.node.node()->AsFunctionCallMut();
orig_func->set_function(
Token(orig_func->function().location(), Token::IDENTIFIER, group_type));
std::vector<std::unique_ptr<ParseNode>> group_stmts;
group_stmts.push_back(build_file.create_assignment(
"public_deps",
build_file.to_node(Value(nullptr, std::move(target_names)))));
for (const auto& assign : target.assignments({"testonly", "visibility"})) {
auto node = assign.node()->Clone();
if (assign.is_conditional()) {
TreeNode({node.get()})
.add_todo(
state, target,
"This was conditional in the original target. Manual review is "
"required to decide if it applies to this group target.");
}
group_stmts.push_back(std::move(node));
}
target.block->statements() = std::move(group_stmts);
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 if (args[0] == "shard") {
if (args.size() == 1) {
return ShardCommand();
} else if (args.size() == 2) {
return ShardCommand(args[1]);
} else if (args.size() == 3) {
return ShardCommand(args[1], args[2]);
} else {
return Err(Location(), "Invalid shard command.",
"Usage: shard [sharded target type] [group type]");
}
} else {
return Err(Location(),
"Unknown edit command: " + std::string(args[0]) +
"\n"
"See `gn help edit` for list of supported commands.");
}
}