blob: 6535c4370770be08ec17e5e70ac887918ffdac14 [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/build_file_editor.h"
#include <algorithm>
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "gn/build_settings.h"
#include "gn/command_format.h"
#include "gn/edit_subcommands.h"
#include "gn/filesystem_utils.h"
#include "gn/input_file.h"
#include "gn/label.h"
#include "gn/loader.h"
#include "gn/parse_tree.h"
#include "gn/parser.h"
#include "gn/scope.h"
#include "gn/string_atom.h"
#include "gn/tokenizer.h"
#include "gn/value.h"
namespace {
std::optional<Value> AsLiteralValue(const ParseNode* node) {
auto* literal = node->AsLiteral();
if (!literal) {
return std::nullopt;
}
Scope scope(static_cast<const Settings*>(nullptr));
Err err;
Value v = literal->Execute(&scope, &err);
// Literals should *usually* not error out, but there are some cases they do.
// Eg. the string literal "${foo}" with no variable foo in scope.
// When this happens, just treat them as if they're opaque things we don't
// know about.
if (err.has_error()) {
return std::nullopt;
}
return v;
}
// Returns true if a node in the tree is a literal node matching the user's
// request.
bool Matches(const EditTarget& target,
const ParseNode* node,
const Value& value) {
auto got_value = AsLiteralValue(node);
if (!got_value) {
return false;
}
if (*got_value == value) {
return true;
}
if (got_value->type() == Value::STRING && value.type() == Value::STRING) {
// If the user requests "remove deps //foo:bar" //foo:baz, and //foo:baz
// contains the literal ":bar", that should match.
Err err;
Label got_label =
Label::Resolve(target.label.dir(), "", target.label.GetToolchainLabel(),
*got_value, &err);
Label want_label = Label::Resolve(
target.label.dir(), "", target.label.GetToolchainLabel(), value, &err);
return !err.has_error() && got_label == want_label;
}
return false;
}
// Finds matching nodes in an expression.
template <typename T>
void FindExpressionRecursive(
ParseNode* node,
std::vector<ParseNode*>& stack,
const std::function<std::optional<T>(TreeNode&)>& transform,
std::vector<T>* results) {
if (!node)
return;
stack.push_back(node);
TreeNode node_ref(stack);
if (auto mapped = transform(node_ref)) {
results->push_back(std::move(*mapped));
}
if (auto* list = node->AsListMut()) {
for (auto& item : list->contents()) {
FindExpressionRecursive(item.get(), stack, transform, results);
}
} else if (auto* op = node->AsBinaryOpMut()) {
if (op->op().type() == Token::PLUS) {
FindExpressionRecursive(op->left(), stack, transform, results);
FindExpressionRecursive(op->right(), stack, transform, results);
}
}
stack.pop_back();
}
// Finds matching nodes in an expression.
template <typename T>
std::vector<T> FindExpression(
const TreeNode& root,
const std::function<std::optional<T>(TreeNode&)>& transform) {
std::vector<T> results;
std::vector<ParseNode*> stack = root.stack();
stack.pop_back();
FindExpressionRecursive<T>(root.node(), stack, transform, &results);
return results;
}
// Resolves a single LabelPattern to matching SourceFiles.
Result<std::vector<SourceFile>> ResolvePatternToFiles(
const BuildSettings* build_settings,
const Loader* loader,
const LabelPattern& pattern) {
std::vector<SourceFile> matched_files;
auto add_dir = [&](const SourceDir& dir) {
auto build_file = loader->BuildFileForLabel(Label(dir, "dummy"));
if (base::PathExists(build_settings->GetFullPath(build_file))) {
matched_files.push_back(build_file);
}
};
add_dir(pattern.dir());
if (pattern.type() == LabelPattern::MATCH ||
pattern.type() == LabelPattern::DIRECTORY) {
if (matched_files.empty()) {
return Err(
Location(),
"Build file does not exist: " +
loader->BuildFileForLabel(Label(pattern.dir(), "dummy")).value());
}
return matched_files;
}
base::FilePath disk_path = build_settings->GetFullPath(pattern.dir());
if (!base::DirectoryExists(disk_path)) {
return Err(Location(),
"Directory does not exist: " + pattern.dir().value());
}
base::FileEnumerator traverser(disk_path, /*recursive=*/true,
base::FileEnumerator::DIRECTORIES);
for (base::FilePath current = traverser.Next(); !current.empty();
current = traverser.Next()) {
base::FilePath relative;
if (build_settings->root_path().AppendRelativePath(current, &relative)) {
std::string source_path = "//" + FilePathToUTF8(relative) + "/";
NormalizePath(&source_path);
add_dir(SourceDir(source_path));
}
}
return matched_files;
}
} // namespace
std::optional<std::string> AsStringLiteral(const ParseNode* node) {
auto val = AsLiteralValue(node);
if (val && val->type() == Value::STRING) {
return std::move(val->string_value());
}
return std::nullopt;
}
std::vector<TreeNode> FindAllListElements(const TreeNode& assignment) {
auto* op = assignment.AsAssignment();
if (!op)
return {};
return FindExpression<TreeNode>(
assignment.Descend(op->right()),
[](TreeNode& node_ref) -> std::optional<TreeNode> {
if (node_ref.parent() && node_ref.parent()->AsList()) {
return node_ref;
}
return std::nullopt;
});
}
std::vector<TreeNode> FindListElementInAssignment(const EditTarget& target,
const TreeNode& root,
const Value& value) {
auto* node = root.AsAssignment();
if (!node)
return {};
return FindExpression<TreeNode>(
root.Descend(node->right()),
[&](TreeNode& node_ref) -> std::optional<TreeNode> {
if (node_ref.parent() && node_ref.parent()->AsList() &&
Matches(target, node_ref.node(), value)) {
return node_ref;
}
return std::nullopt;
});
}
std::optional<ListNode*> FindListInAssignment(const TreeNode& assignment) {
auto* op = assignment.AsAssignment();
if (!op)
return std::nullopt;
auto results = FindExpression<ListNode*>(
assignment.Descend(op->right()),
[](TreeNode& node_ref) -> std::optional<ListNode*> {
if (auto* list = node_ref->AsListMut()) {
return list;
}
return std::nullopt;
});
if (results.empty())
return std::nullopt;
return results.front();
}
TreeNode TreeNode::Descend(ParseNode* child) const {
std::vector<ParseNode*> s = stack_;
s.push_back(child);
return TreeNode(std::move(s));
}
BinaryOpNode* TreeNode::AsAssignment() const {
if (auto* op = node()->AsBinaryOpMut()) {
if (op->op().type() == Token::EQUAL ||
op->op().type() == Token::PLUS_EQUALS) {
return op;
}
}
return nullptr;
}
bool TreeNode::is_conditional() const {
DCHECK(!stack_.empty()) << "stack should never be empty";
for (auto it = stack_.rbegin() + 1; it != stack_.rend(); ++it) {
if ((*it)->AsCondition()) {
return true;
}
// Stop at the target boundary.
if ((*it)->AsFunctionCall()) {
break;
}
}
return false;
}
bool TreeNode::is_modification() const {
if (const auto* op = node()->AsBinaryOp()) {
return op->op().type() == Token::PLUS_EQUALS ||
op->op().type() == Token::MINUS_EQUALS;
}
return false;
}
void TreeNode::add_todo(EditState& state,
const EditTarget& target,
std::string_view message) const {
std::string line =
"# TODO(gn edit: " + state.context + "): " + std::string(message);
StringAtom atom(line);
Token comment_token(node()->GetRange().begin(), Token::LINE_COMMENT,
atom.str());
node()->comments_mutable()->append_before(std::move(comment_token));
state.needs_manual_review.insert(target.label);
}
void TreeNode::RemoveSelf(EditState& state, const EditTarget& target) const {
if (is_conditional()) {
add_todo(state, target,
"This would normally be deleted but is conditional. Manual "
"intervention is required to decide whether it should actually be "
"deleted.");
} else {
RemoveSelfUnconditionally();
}
}
TreeNode::NodeList& TreeNode::container() const {
DCHECK(parent());
if (auto* block = parent()->AsBlockMut()) {
return block->statements();
} else if (auto* list = parent()->AsListMut()) {
return list->contents();
} else {
NOTREACHED() << "Unsupported parent type in container";
}
}
TreeNode::NodeLocation TreeNode::node_location() const {
auto& c = container();
auto it = std::find_if(c.begin(), c.end(),
[this](const auto& p) { return p.get() == node(); });
CHECK(it != c.end()) << "child node not found in parent container";
return {c, it};
}
void TreeNode::RemoveSelfUnconditionally() const {
auto [container, it] = node_location();
container.erase(it);
}
LabelMatcher::LabelMatcher(SourceDir source_dir,
const std::vector<LabelPattern>& patterns)
: source_dir_(std::move(source_dir)), globbed_(false) {
for (const auto& pattern : patterns) {
if (pattern.type() == LabelPattern::RECURSIVE_DIRECTORY &&
source_dir_.value().starts_with(pattern.dir().value())) {
globbed_ = true;
} else if (pattern.type() == LabelPattern::DIRECTORY &&
source_dir_ == pattern.dir()) {
globbed_ = true;
} else if (pattern.type() == LabelPattern::MATCH &&
pattern.dir() == source_dir_) {
if (!used_.contains(pattern.name())) {
explicit_names_.push_back(pattern.name());
used_[pattern.name()] = false;
}
}
}
}
LabelMatcher::MatchType LabelMatcher::matches(const std::string& name) {
if (auto it = used_.find(name); it != used_.end()) {
it->second = true; // Mark as used.
return EXACT;
}
return globbed_ ? GLOB : NONE;
}
Result<std::vector<std::string>> LabelMatcher::explicit_target_names() {
if (globbed_) {
return Err(Location(),
"Explicit target label required (wildcard patterns are not "
"supported for this command).");
}
for (const auto& name : explicit_names_) {
used_[name] = true;
}
return explicit_names_;
}
Err LabelMatcher::done() const {
std::vector<std::string> unused;
for (const auto& [name, used] : used_) {
if (!used) {
unused.push_back(name);
}
}
if (!unused.empty()) {
std::sort(unused.begin(), unused.end());
std::string msg = "Target(s) not found: ";
for (size_t i = 0; i < unused.size(); ++i) {
if (i > 0)
msg += ", ";
msg += Label(source_dir_, unused[i]).GetUserVisibleName(false);
}
return Err(Location(), msg);
}
return Ok();
}
std::vector<TreeNode> TreeNode::assignments(
std::initializer_list<std::string_view> attrs) const {
return FindStatement<TreeNode>(
node(), [attrs](TreeNode& node_ref) -> std::optional<TreeNode> {
if (const auto* op = node_ref->AsBinaryOp()) {
if (op->op().type() == Token::EQUAL ||
op->op().type() == Token::PLUS_EQUALS ||
op->op().type() == Token::MINUS_EQUALS) {
if (const auto* left = op->left()->AsIdentifier()) {
for (auto attr : attrs) {
if (left->value().value() == attr) {
return node_ref;
}
}
}
}
}
return std::nullopt;
});
}
std::vector<TreeNode> TreeNode::assignments(std::string_view attr) const {
return assignments({attr});
}
std::vector<TreeNode> EditTarget::assignments(
std::initializer_list<std::string_view> attrs) const {
return node.Descend(block).assignments(attrs);
}
std::vector<TreeNode> EditTarget::assignments(std::string_view attr) const {
return assignments({attr});
}
void EditTarget::add_warning(EditState& state, std::string_view message) const {
std::string full_message = "Target \"" + label.GetUserVisibleName(false) +
"\" " + std::string(message);
state.warnings.push_back(Err(node.node()->GetRange().begin(), full_message));
}
Result<BuildFile> BuildFile::Create(const BuildSettings* build_settings,
const SourceFile& source_file,
const std::vector<LabelPattern>& patterns) {
auto input_file = std::make_unique<InputFile>(source_file);
base::FilePath full_path = build_settings->GetFullPath(source_file);
if (!input_file->Load(full_path)) {
return Err(Location(), "Could not load file: " + source_file.value());
}
Err err;
std::vector<Token> tokens = Tokenizer::Tokenize(input_file.get(), &err);
RETURN_IF_ERROR(err);
std::unique_ptr<ParseNode> tree_root = Parser::Parse(tokens, &err);
RETURN_IF_ERROR(err);
LabelMatcher label_matcher(source_file.GetDir(), patterns);
return BuildFile(build_settings, source_file, std::move(input_file),
std::move(tree_root), std::move(label_matcher));
}
Location BuildFile::location() const {
return Location(input_file_.get(), 1, 1);
}
std::vector<EditTarget> BuildFile::targets(
std::function<bool(EditTarget&)> filter) {
if (!filter) {
filter = [this](EditTarget& t) {
switch (label_matcher_.matches(t.label.name())) {
case LabelMatcher::NONE:
return false;
case LabelMatcher::EXACT:
return true;
case LabelMatcher::GLOB:
t.is_explicit = false;
return true;
}
NOTREACHED();
};
}
return FindStatement<EditTarget>(
tree_root_.get(),
[this, &filter](TreeNode& node_ref) -> std::optional<EditTarget> {
if (auto* func = node_ref->AsFunctionCallMut()) {
if (func->block() && func->args() &&
func->args()->contents().size() == 1) {
if (auto name =
AsStringLiteral(func->args()->contents()[0].get())) {
EditTarget target{
.is_explicit = true,
.label = Label(source_file_.GetDir(), *name),
.node = node_ref,
.block = func->block(),
};
if (filter(target)) {
return target;
}
}
}
}
return std::nullopt;
});
}
std::optional<EditTarget> BuildFile::find_target(std::string_view target_name) {
auto found = targets([target_name](const EditTarget& t) {
return t.label.name() == target_name;
});
if (!found.empty()) {
return std::move(found.front());
}
return std::nullopt;
}
std::unique_ptr<ParseNode> BuildFile::to_node(const Value& value) {
auto file = std::make_unique<InputFile>(SourceFile("//dummy"));
file->SetContents(value.ToString(true));
Err err;
std::vector<Token> tokens = Tokenizer::Tokenize(file.get(), &err);
for (auto& token : tokens) {
token.set_location(this->location());
}
auto parsed = Parser::ParseExpression(tokens, &err);
extra_files_.push_back(std::move(file));
// value.ToString() must return something parsable as input to GN.
DCHECK(!err.has_error());
return parsed;
}
std::unique_ptr<IdentifierNode> BuildFile::create_identifier(
std::string_view value) {
StringAtom atom(value);
return std::make_unique<IdentifierNode>(
Token(location(), Token::IDENTIFIER, atom.str()));
}
std::unique_ptr<BinaryOpNode> BuildFile::create_assignment(
std::string_view name,
std::unique_ptr<ParseNode> value) {
auto left = create_identifier(name);
auto assign = std::make_unique<BinaryOpNode>();
assign->set_op(Token(location(), Token::EQUAL, "="));
assign->set_left(std::move(left));
assign->set_right(std::move(value));
return assign;
}
std::unique_ptr<BlockNode> BuildFile::create_block(
std::vector<std::unique_ptr<ParseNode>> statements) {
auto block = std::make_unique<BlockNode>(BlockNode::DISCARDS_RESULT);
block->set_begin_token(Token(location(), Token::LEFT_BRACE, "{"));
block->set_end(
std::make_unique<EndNode>(Token(location(), Token::RIGHT_BRACE, "}")));
block->statements() = std::move(statements);
return block;
}
std::unique_ptr<FunctionCallNode> BuildFile::create_target(
std::string_view type,
std::string_view name,
std::unique_ptr<BlockNode> block) {
auto func = std::make_unique<FunctionCallNode>();
func->set_function(
Token(location(), Token::IDENTIFIER, StringAtom(type).str()));
auto args = std::make_unique<ListNode>();
args->set_begin_token(Token(location(), Token::LEFT_PAREN, "("));
args->set_end(
std::make_unique<EndNode>(Token(location(), Token::RIGHT_PAREN, ")")));
args->append_item(to_node(Value(nullptr, std::string(name))));
func->set_args(std::move(args));
func->set_block(std::move(block));
return func;
}
Result<bool> BuildFile::Write() {
ASSIGN_OR_RETURN(std::string formatted, commands::FormatNodeToString(root()));
if (input_file_->contents() == formatted) {
return false;
}
base::FilePath file_path = build_settings_->GetFullPath(source_file());
if (base::WriteFile(file_path, formatted.data(),
static_cast<int>(formatted.size())) == -1) {
return Err(Location(),
"Failed to write to file: " + FilePathToUTF8(file_path));
}
return true;
}
BuildFile::BuildFile(const BuildSettings* build_settings,
SourceFile source_file,
std::unique_ptr<InputFile> input_file,
std::unique_ptr<ParseNode> tree_root,
LabelMatcher label_matcher)
: build_settings_(build_settings),
source_file_(std::move(source_file)),
input_file_(std::move(input_file)),
tree_root_(std::move(tree_root)),
label_matcher_(std::move(label_matcher)) {}
Result<std::vector<BuildFile>> ResolvePatternsToBuildFiles(
const BuildSettings* build_settings,
const Loader* loader,
const std::vector<LabelPattern>& patterns) {
std::set<SourceFile> seen;
std::vector<BuildFile> result;
for (const LabelPattern& pattern : patterns) {
ASSIGN_OR_RETURN(auto files,
ResolvePatternToFiles(build_settings, loader, pattern));
for (const SourceFile& file : files) {
if (seen.insert(file).second) {
ASSIGN_OR_RETURN(auto parsed,
BuildFile::Create(build_settings, file, patterns));
result.push_back(std::move(parsed));
}
}
}
return result;
}