Support conditional sources for gn check.
Suppose you have foo_linux.cc including bar.h, and:
```
source_set("foo") {
sources = [ "foo.cc" ]
if (is_linux) {
sources += [ "foo_linux.cc" ]
}
}
```
Previously, we would suggest `gn edit "add deps //:bar"` //:foo`.
Now, we no longer output the gn edit suggestion, and instead suggest
doing so inside an is_linux conditional.
Change-Id: Iac7fb49959fb6437b5ffd2595614bf526a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/26680
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: Matt Stark <msta@google.com>
diff --git a/src/gn/build_file_editor.cc b/src/gn/build_file_editor.cc
index 3e3ec0e..a471447 100644
--- a/src/gn/build_file_editor.cc
+++ b/src/gn/build_file_editor.cc
@@ -8,6 +8,7 @@
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
+#include "base/strings/string_util.h"
#include "gn/build_settings.h"
#include "gn/command_format.h"
#include "gn/edit_subcommands.h"
@@ -373,6 +374,60 @@
return false;
}
+std::optional<StringAtom> TreeNode::GetConditionString() const {
+ if (!is_conditional()) {
+ return std::nullopt;
+ }
+
+ std::vector<std::string> clauses;
+ std::vector<const ConditionNode*> chain;
+
+ auto get_condition_text = [](const ConditionNode* node) {
+ return base::CollapseWhitespaceASCII(
+ std::string(node->condition()->GetRange().GetText()), false);
+ };
+
+ for (const ParseNode* node : stack_) {
+ if (node->AsFunctionCall()) {
+ // Remove the conditionals outside the target definition.
+ clauses.clear();
+ chain.clear();
+ continue;
+ }
+
+ const auto* condition = node->AsCondition();
+ if (!chain.empty() &&
+ (condition == nullptr || chain.back()->if_false() != condition)) {
+ std::string head = get_condition_text(chain.front());
+ if (node == chain.back()->if_false()) {
+ if (chain.size() == 1) {
+ clauses.push_back("if (" + head + ")'s else");
+ } else if (chain.size() == 2) {
+ clauses.push_back("if (" + head + ")'s else (after else if)");
+ } else {
+ clauses.push_back("if (" + head + ")'s else (after else ifs)");
+ }
+ } else if (chain.size() == 1) {
+ clauses.push_back("if (" + head + ")");
+ } else {
+ clauses.push_back("if (" + head + ")'s else if (" +
+ get_condition_text(chain.back()) + ")");
+ }
+ chain.clear();
+ }
+
+ if (condition) {
+ chain.push_back(condition);
+ }
+ }
+
+ if (clauses.empty()) {
+ return std::nullopt;
+ }
+
+ return StringAtom(base::JoinString(clauses, " -> "));
+}
+
bool TreeNode::is_modification() const {
if (const auto* op = node()->AsBinaryOp()) {
return op->op().type() == Token::PLUS_EQUALS ||
@@ -774,3 +829,29 @@
}
return result;
}
+
+TargetSourcesMap GetSourcesForTargets(BuildFile& build_file) {
+ TargetSourcesMap result;
+ for (const auto& target :
+ build_file.targets([](EditTarget&) { return true; })) {
+ SourceConditionsMap source_conditions;
+ for (const auto& assignment : target.assignments({"sources", "public"})) {
+ auto condition = assignment.GetConditionString();
+ for (const auto& element : FindAllListElements(assignment)) {
+ auto str = AsStringLiteral(element.node());
+ if (!str)
+ continue;
+ Err err;
+ SourceFile source_file =
+ build_file.source_file().GetDir().ResolveRelativeFile(
+ Value(nullptr, *str), &err);
+ if (err.has_error() || source_file.is_null())
+ continue;
+
+ source_conditions.try_emplace(source_file, condition);
+ }
+ }
+ result.emplace(target.label.name_atom(), std::move(source_conditions));
+ }
+ return result;
+}
diff --git a/src/gn/build_file_editor.h b/src/gn/build_file_editor.h
index 8872f29..b62174d 100644
--- a/src/gn/build_file_editor.h
+++ b/src/gn/build_file_editor.h
@@ -19,6 +19,7 @@
#include "gn/label_pattern.h"
#include "gn/parse_tree.h"
#include "gn/source_file.h"
+#include "gn/string_atom.h"
class BuildSettings;
class Loader;
@@ -26,6 +27,10 @@
struct EditState;
struct EditTarget;
+using SourceConditionsMap =
+ std::unordered_map<SourceFile, std::optional<StringAtom>>;
+using TargetSourcesMap = std::unordered_map<StringAtom, SourceConditionsMap>;
+
// A TreeNode represents a node in a tree.
// It fundamentally represents a ParseNode, but differs from one as it
// understands where it exists in the tree.
@@ -47,6 +52,10 @@
// Note that if the target itself is conditional, this will return false.
bool is_conditional() const;
+ // Returns the human-readable condition string if this node is inside a
+ // conditional statement within a target (e.g. "if (foo)'s else -> if (bar)").
+ std::optional<StringAtom> GetConditionString() const;
+
// Returns whether the node is a "+=" or "-=" operation.
bool is_modification() const;
@@ -301,4 +310,8 @@
const Loader* loader,
const std::vector<LabelPattern>& patterns);
+// Extracts all targets in the build file along with their sources and any
+// enclosing conditions.
+TargetSourcesMap GetSourcesForTargets(BuildFile& build_file);
+
#endif // TOOLS_GN_BUILD_FILE_EDITOR_H_
diff --git a/src/gn/command_suggest.cc b/src/gn/command_suggest.cc
index fab7418..28b21f6 100644
--- a/src/gn/command_suggest.cc
+++ b/src/gn/command_suggest.cc
@@ -16,6 +16,7 @@
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/strings/string_split.h"
+#include "gn/build_file_editor.h"
#include "gn/commands.h"
#include "gn/config_values_extractors.h"
#include "gn/edit_command.h"
@@ -104,17 +105,40 @@
// Finds all targets that use a file as a source from a specific toolchain and
// adds them to results. Checks every toolchain if current_toolchain is null.
-bool AddToolchainSources(
- const std::vector<const Target*>& all_targets,
- const Label* current_toolchain,
- const SourceFile& file,
- TargetResolutionCache& cache,
- std::vector<std::pair<const Target*, commands::ApiScope>>& results) {
+bool AddToolchainSources(const std::vector<const Target*>& all_targets,
+ const Label* current_toolchain,
+ const SourceFile& file,
+ const BuildSettings* build_settings,
+ TargetResolutionCache& cache,
+ std::vector<ResolvedTarget>& results) {
for (const auto& [target, scope] :
cache.GetTargetsForFile(file, all_targets)) {
if (!current_toolchain ||
target->label().GetToolchainLabel() == *current_toolchain) {
- results.emplace_back(target, scope);
+ // Gracefully fall back to assuming it's unconditional if the build file
+ // is badly formatted or the source was added via a .gni file, for
+ // example.
+ std::optional<StringAtom> condition;
+ Err err;
+ SourceFile build_file = target->label().dir().ResolveRelativeFile(
+ Value(nullptr, "BUILD.gn"), &err);
+ if (!err.has_error() && !build_file.is_null()) {
+ if (const auto* sources_map =
+ cache.GetSourcesForBuildFile(build_file, build_settings)) {
+ auto target_it = sources_map->find(target->label().name_atom());
+ if (target_it != sources_map->end()) {
+ auto source_it = target_it->second.find(file);
+ if (source_it != target_it->second.end()) {
+ condition = source_it->second;
+ }
+ }
+ }
+ }
+ results.push_back(ResolvedTarget{
+ .target = target,
+ .scope = scope,
+ .conditional = std::move(condition),
+ });
}
}
return !results.empty();
@@ -303,6 +327,23 @@
return *it->second;
}
+const TargetSourcesMap* TargetResolutionCache::GetSourcesForBuildFile(
+ const SourceFile& build_file,
+ const BuildSettings* build_settings) {
+ std::lock_guard<std::mutex> lock(sources_cache_lock_);
+ auto it = build_file_sources_cache_.find(build_file);
+ if (it != build_file_sources_cache_.end()) {
+ return &it->second;
+ }
+ auto parsed = BuildFile::Create(build_settings, build_file, {});
+ if (parsed.has_error()) {
+ return nullptr;
+ }
+ auto [inserted, _] = build_file_sources_cache_.emplace(
+ build_file, GetSourcesForTargets(*parsed));
+ return &inserted->second;
+}
+
// Resolves an input to a list of targets, and whether each are private.
// The input can be:
// * A module name for a target
@@ -311,20 +352,20 @@
// * Targets defined in the current toolchain that contain the file
// * Targets defined in the default toolchain that contain the file
// * Targets defined in any toolchain that contain the file
-std::pair<std::vector<std::pair<const Target*, commands::ApiScope>>, bool>
-ResolveSuggestionToTarget(const BuildSettings* build_settings,
- const std::vector<const Target*>& all_targets,
- const Label& current_toolchain,
- std::string_view input,
- bool must_be_file,
- TargetResolutionCache& cache,
- const Target* includer) {
+std::pair<std::vector<ResolvedTarget>, bool> ResolveSuggestionToTarget(
+ const BuildSettings* build_settings,
+ const std::vector<const Target*>& all_targets,
+ const Label& current_toolchain,
+ std::string_view input,
+ bool must_be_file,
+ TargetResolutionCache& cache,
+ const Target* includer) {
auto sort_results = [](auto& vec) {
std::sort(vec.begin(), vec.end(), [](const auto& lhs, const auto& rhs) {
- return lhs.first->label() < rhs.first->label();
+ return lhs.target->label() < rhs.target->label();
});
};
- std::vector<std::pair<const Target*, commands::ApiScope>> results;
+ std::vector<ResolvedTarget> results;
if (!must_be_file) {
std::string_view module_name = input;
commands::ApiScope is_private = commands::ApiScope::kPublic;
@@ -336,7 +377,7 @@
// Try to resolve as a module name.
for (const Target* target : all_targets) {
if (target->module_name() == module_name) {
- results.emplace_back(target, is_private);
+ results.push_back(ResolvedTarget{target, is_private, std::nullopt});
}
}
if (!results.empty()) {
@@ -354,7 +395,7 @@
if (!err.has_error()) {
for (const Target* target : all_targets) {
if (target->label() == want) {
- results.emplace_back(target, is_private);
+ results.push_back(ResolvedTarget{target, is_private, std::nullopt});
// We know each label corresponds to exactly one target, so we don't
// need to keep going.
return {results, true};
@@ -373,21 +414,22 @@
// If we see //foo(:toolchain) request bar.h, prefer //:bar(:toolchain)
// over other toolchains.
- if (!AddToolchainSources(all_targets, ¤t_toolchain, file, cache,
- results)) {
- AddToolchainSources(all_targets, nullptr, file, cache, results);
+ if (!AddToolchainSources(all_targets, ¤t_toolchain, file,
+ build_settings, cache, results)) {
+ AddToolchainSources(all_targets, nullptr, file, build_settings, cache,
+ results);
}
// If we have an action that generates "gen/foo.h", we should prefer
// depending on the source set that declares it as a header.
if (std::ranges::all_of(results, [](const auto& result) {
- return result.second == commands::ApiScope::kOutput;
+ return result.scope == commands::ApiScope::kOutput;
})) {
for (auto& result : results) {
- result.second = commands::ApiScope::kPublic;
+ result.scope = commands::ApiScope::kPublic;
}
} else {
std::erase_if(results, [](const auto& result) {
- return result.second == commands::ApiScope::kOutput;
+ return result.scope == commands::ApiScope::kOutput;
});
}
sort_results(results);
@@ -575,8 +617,8 @@
return SuggestResult::kFailure;
if (includer_target) {
- std::erase_if(includer_targets, [&](const auto& pair) {
- return pair.first != includer_target;
+ std::erase_if(includer_targets, [&](const auto& match) {
+ return match.target != includer_target;
});
}
@@ -589,14 +631,16 @@
StartError();
OutputQuoted(includer_name);
OutputString(" resolved to multiple targets\n");
- for (const auto& [target, is_private] : includer_targets) {
+ for (const auto& match : includer_targets) {
OutputString("* ");
- OutputTarget(target);
+ OutputTarget(match.target);
OutputString("\n");
}
return SuggestResult::kFailure;
}
- const auto& [includer, dep_kind] = includer_targets.front();
+ const auto& includer_match = includer_targets.front();
+ const Target* includer = includer_match.target;
+ commands::ApiScope dep_kind = includer_match.scope;
current_toolchain = includer->label().GetToolchainLabel();
std::string_view dep_field =
@@ -622,11 +666,12 @@
}
std::set<Label> labels_without_toolchain;
- for (const auto& [target, _] : targets) {
- labels_without_toolchain.insert(target->label().GetWithNoToolchain());
+ for (const auto& match : targets) {
+ labels_without_toolchain.insert(match.target->label().GetWithNoToolchain());
}
if (labels_without_toolchain.size() == 1 &&
- targets.front().first->label().GetToolchainLabel() != current_toolchain) {
+ targets.front().target->label().GetToolchainLabel() !=
+ current_toolchain) {
// The resolution requires that if //:bar(:toolchain1) contained bar.h, we
// would have returned no targets from any other toolchain. Thus, we now
// have:
@@ -641,7 +686,7 @@
OutputString("\n");
SourceFile file = ResolveFilePath(build_settings, all_targets,
included_name, cache, includer);
- const Target* target = targets.front().first;
+ const Target* target = targets.front().target;
std::string path = file.is_null()
? std::string(included_name)
: RebasePath(file.value(), target->label().dir(),
@@ -658,9 +703,9 @@
StartWarning();
OutputQuoted(included_name);
OutputString(" is ambiguous because it belongs to multiple targets:\n");
- for (const auto& [target, _] : targets) {
+ for (const auto& match : targets) {
OutputString("* ");
- OutputTarget(target);
+ OutputTarget(match.target);
OutputString("\n");
}
StartSuggestion();
@@ -675,7 +720,8 @@
return result;
}
- const auto& [included, included_dep_kind] = targets.front();
+ const Target* included = targets.front().target;
+ commands::ApiScope included_dep_kind = targets.front().scope;
if (included_dep_kind == commands::ApiScope::kPrivate) {
StartWarning();
OutputQuoted(included_name);
@@ -873,6 +919,37 @@
return std::tie(lhs_abs, lhs.label) <
std::tie(rhs_abs, rhs.label);
});
+ if (includer_match.conditional) {
+ if (apply) {
+ result = SuggestResult::kUnapplied;
+ }
+ StartSuggestion();
+ if (candidate_deps.size() == 1) {
+ OutputString("Under ");
+ OutputString(includer_match.conditional->str());
+ OutputString(", add ");
+ OutputString(dep_field);
+ OutputString(" = [ ");
+ OutputQuoted(candidate_deps.front().label);
+ OutputString(" ] to ");
+ OutputDefinition(includer);
+ OutputString("\n");
+ } else {
+ SetAmbiguous();
+ OutputString("Under ");
+ OutputString(includer_match.conditional->str());
+ OutputString(", add one of the following to ");
+ OutputString(dep_field);
+ OutputString(" in ");
+ OutputDefinition(includer);
+ OutputString(":\n");
+ for (const auto& c : candidate_deps) {
+ OutputString("* " + c.label + "\n");
+ }
+ }
+ return;
+ }
+
if (candidate_deps.size() == 1) {
OutputEditCommand(candidate_deps.front().edit, includer);
return;
diff --git a/src/gn/command_suggest_unittest.cc b/src/gn/command_suggest_unittest.cc
index eda3a32..4ddc70c 100644
--- a/src/gn/command_suggest_unittest.cc
+++ b/src/gn/command_suggest_unittest.cc
@@ -111,7 +111,7 @@
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, default_toolchain,
"my_module", /*must_be_file=*/false, cache);
- std::vector<std::pair<const Target*, commands::ApiScope>> expected = {
+ std::vector<commands::ResolvedTarget> expected = {
{&target, commands::ApiScope::kPublic}};
EXPECT_EQ(expected, results);
EXPECT_TRUE(ok);
@@ -122,7 +122,7 @@
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, default_toolchain,
"my_module_Private", /*must_be_file=*/false, cache);
- std::vector<std::pair<const Target*, commands::ApiScope>> expected = {
+ std::vector<commands::ResolvedTarget> expected = {
{&target, commands::ApiScope::kPrivate}};
EXPECT_EQ(expected, results);
EXPECT_TRUE(ok);
@@ -151,7 +151,7 @@
setup_scope.toolchain()->label(), "//:hello", /*must_be_file=*/false,
cache);
- std::vector<std::pair<const Target*, commands::ApiScope>> expected_label = {
+ std::vector<commands::ResolvedTarget> expected_label = {
{&target, commands::ApiScope::kPublic}};
EXPECT_EQ(expected_label, results_label);
EXPECT_TRUE(ok_label);
@@ -161,8 +161,8 @@
setup_scope.build_settings(), all_targets, default_toolchain,
"//:hello(//build/toolchain:gcc)", /*must_be_file=*/false, cache);
- std::vector<std::pair<const Target*, commands::ApiScope>> expected_toolchain =
- {{&target_gcc, commands::ApiScope::kPublic}};
+ std::vector<commands::ResolvedTarget> expected_toolchain = {
+ {&target_gcc, commands::ApiScope::kPublic}};
EXPECT_EQ(expected_toolchain, results_toolchain);
EXPECT_TRUE(ok_toolchain);
}
@@ -269,7 +269,7 @@
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"//public.h", /*must_be_file=*/true, cache);
- std::vector<std::pair<const Target*, commands::ApiScope>> expected = {
+ std::vector<commands::ResolvedTarget> expected = {
{&explicit_target, commands::ApiScope::kPublic}};
EXPECT_TRUE(ok);
EXPECT_EQ(expected, results);
@@ -279,7 +279,7 @@
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"../../private.h", /*must_be_file=*/true, cache);
- std::vector<std::pair<const Target*, commands::ApiScope>> expected = {
+ std::vector<commands::ResolvedTarget> expected = {
{&explicit_target, commands::ApiScope::kPrivate}};
EXPECT_TRUE(ok);
EXPECT_EQ(expected, results);
@@ -289,7 +289,7 @@
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"//implicit_public.h", /*must_be_file=*/true, cache);
- std::vector<std::pair<const Target*, commands::ApiScope>> expected = {
+ std::vector<commands::ResolvedTarget> expected = {
{&implicit_target, commands::ApiScope::kPublic}};
EXPECT_TRUE(ok);
EXPECT_EQ(expected, results);
@@ -306,7 +306,7 @@
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"//out/Debug/generated_file.h", /*must_be_file=*/true, cache);
- std::vector<std::pair<const Target*, commands::ApiScope>> expected = {
+ std::vector<commands::ResolvedTarget> expected = {
{&generated, commands::ApiScope::kPublic}};
EXPECT_TRUE(ok);
EXPECT_EQ(expected, results);
@@ -318,7 +318,7 @@
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"//out/Debug/generated_file.h", /*must_be_file=*/true, consumer_cache);
- std::vector<std::pair<const Target*, commands::ApiScope>> expected = {
+ std::vector<commands::ResolvedTarget> expected = {
{&consumer, commands::ApiScope::kPublic}};
EXPECT_TRUE(ok);
EXPECT_EQ(expected, results);
@@ -328,7 +328,7 @@
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"//no_target.h", /*must_be_file=*/true, consumer_cache);
- std::vector<std::pair<const Target*, commands::ApiScope>> expected_targets;
+ std::vector<commands::ResolvedTarget> expected_targets;
EXPECT_TRUE(ok);
EXPECT_EQ(expected_targets, results);
}
@@ -337,10 +337,9 @@
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"//default_toolchain.h", /*must_be_file=*/true, consumer_cache);
- std::vector<std::pair<const Target*, commands::ApiScope>> expected_targets =
- {
- {&simple_secondary, commands::ApiScope::kPublic},
- {&simple_default, commands::ApiScope::kPublic},
+ std::vector<commands::ResolvedTarget> expected_targets = {
+ {&simple_secondary, commands::ApiScope::kPublic},
+ {&simple_default, commands::ApiScope::kPublic},
};
EXPECT_TRUE(ok);
EXPECT_EQ(expected_targets, results);
@@ -350,8 +349,8 @@
auto [results, ok] = commands::ResolveSuggestionToTarget(
setup_scope.build_settings(), all_targets, current_toolchain,
"//secondary_toolchain.h", /*must_be_file=*/true, consumer_cache);
- std::vector<std::pair<const Target*, commands::ApiScope>> expected_targets =
- {{{&simple_secondary, commands::ApiScope::kPublic}}};
+ std::vector<commands::ResolvedTarget> expected_targets = {
+ {&simple_secondary, commands::ApiScope::kPublic}};
EXPECT_TRUE(ok);
EXPECT_EQ(expected_targets, results);
}
@@ -361,8 +360,8 @@
setup_scope.build_settings(), all_targets, current_toolchain,
"my_header.h", /*must_be_file=*/true, consumer_cache, &consumer);
EXPECT_TRUE(ok);
- std::vector<std::pair<const Target*, commands::ApiScope>> expected_targets =
- {{{&included_target, commands::ApiScope::kPublic}}};
+ std::vector<commands::ResolvedTarget> expected_targets = {
+ {&included_target, commands::ApiScope::kPublic}};
EXPECT_EQ(expected_targets, results);
}
}
@@ -613,8 +612,13 @@
TEST_F(SuggestTest, ApplyValidSuggestion) {
TestProject project({
- {SourceFile("//BUILD.gn"), R"(executable("includer") {
+ {SourceFile("//BUILD.gn"), R"(is_win = true
+
+executable("includer") {
sources = [ "includer.cc" ]
+ if (is_win) {
+ sources += [ "win.cc" ]
+ }
}
source_set("included") {
@@ -623,6 +627,7 @@
}
)"},
{SourceFile("//includer.cc"), ""},
+ {SourceFile("//win.cc"), ""},
{SourceFile("//included.h"), ""},
{SourceFile("//private.h"), ""},
});
@@ -642,11 +647,16 @@
EXPECT_EQ(commands::SuggestResult::kSuccess, result);
EXPECT_EQ(
"[APPLIED] Suggestion: Add deps = [ \":included\" ] to :includer "
- "(defined at //BUILD.gn:1)\n",
+ "(defined at //BUILD.gn:3)\n",
output);
- std::string expected_build_gn = R"(executable("includer") {
+ std::string expected_build_gn = R"(is_win = true
+
+executable("includer") {
sources = [ "includer.cc" ]
+ if (is_win) {
+ sources += [ "win.cc" ]
+ }
deps = [ ":included" ]
}
@@ -658,11 +668,10 @@
EXPECT_EQ(expected_build_gn, project.Read(SourceFile("//BUILD.gn")));
output.clear();
- commands::TargetResolutionCache same_target_cache;
commands::SuggestResult same_target_result = commands::OutputSuggestions(
project.targets(), &project.setup.build_settings(),
project.default_toolchain(), "//included.h",
- /*includer_target=*/nullptr, "//private.h", collect, same_target_cache,
+ /*includer_target=*/nullptr, "//private.h", collect, cache,
/*must_be_file=*/false, /*apply=*/true, &project.setup);
EXPECT_EQ(commands::SuggestResult::kUnapplied, same_target_result);
@@ -670,13 +679,28 @@
R"(Warning: "//private.h" is in the private API of //:included
[AMBIGUOUS] Suggestion: Choose one of the following to resolve the intra-target include:
* Create a new source_set for "private.h" and add it to public_deps in //:included (preferred)
-* Move "private.h" from `sources` to `public` in :included (defined at //BUILD.gn:5)
+* Move "private.h" from `sources` to `public` in :included (defined at //BUILD.gn:10)
(`gn edit "move sources public private.h" //:included`)
-* Move "included.h" from `public` to `sources` in :included (defined at //BUILD.gn:5)
+* Move "included.h" from `public` to `sources` in :included (defined at //BUILD.gn:10)
(`gn edit "move public sources included.h" //:included`)
)",
output);
EXPECT_EQ(expected_build_gn, project.Read(SourceFile("//BUILD.gn")));
+
+ output.clear();
+
+ commands::SuggestResult conditional_result = commands::OutputSuggestions(
+ project.targets(), &project.setup.build_settings(),
+ project.default_toolchain(), "//win.cc",
+ /*includer_target=*/nullptr, "//included.h", collect, cache,
+ /*must_be_file=*/false, /*apply=*/true, &project.setup);
+
+ EXPECT_EQ(commands::SuggestResult::kUnapplied, conditional_result);
+ EXPECT_EQ(
+ "Suggestion: Under if (is_win), add deps = [ \":included\" ] to "
+ ":includer (defined at //BUILD.gn:3)\n",
+ output);
+ EXPECT_EQ(expected_build_gn, project.Read(SourceFile("//BUILD.gn")));
}
TEST_F(SuggestTest, CheckAppliesSuggestions) {
diff --git a/src/gn/commands.h b/src/gn/commands.h
index 168cb26..08917b9 100644
--- a/src/gn/commands.h
+++ b/src/gn/commands.h
@@ -17,6 +17,7 @@
#include <vector>
#include "base/values.h"
+#include "gn/build_file_editor.h"
#include "gn/header_checker.h"
#include "gn/standard_out.h"
#include "gn/target.h"
@@ -130,6 +131,26 @@
kOutput,
};
+// Represents something that has resolved to a target.
+// May be a source file, a module name, or a target label.
+struct ResolvedTarget {
+ // Can never be null. Unfortunately can't use a reference because it's stored
+ // in a vector.
+ const Target* target;
+ ApiScope scope;
+ // If the input was a conditional source, this is a human-readable
+ // representation of the condition. eg. When looking up foo.cc in a target
+ // with: if (foo) {
+ // if (bar) {
+ // sources += [ "foo.cc" ]
+ // }
+ // }
+ // Conditional could be "if (foo) and if (bar)"
+ std::optional<StringAtom> conditional = std::nullopt;
+
+ bool operator==(const ResolvedTarget&) const = default;
+};
+
// Caches the mapping of SourceFile -> target(s) to accelerate suggestion
// resolution.
// May potentially be used in the future to cache other properties of the build
@@ -154,6 +175,12 @@
// Returns reference to ReachabilityCache for the given target.
HeaderChecker::ReachabilityCache& GetReachabilityCache(const Target* target);
+ // Returns cached source-to-condition map for all targets in the given build
+ // file.
+ const TargetSourcesMap* GetSourcesForBuildFile(
+ const SourceFile& build_file,
+ const BuildSettings* build_settings);
+
private:
std::once_flag file_to_target_initialized_;
std::unordered_map<SourceFile,
@@ -172,6 +199,11 @@
std::unordered_map<const Target*,
std::unique_ptr<HeaderChecker::ReachabilityCache>>
reachability_cache_;
+
+ // Maps a BUILD.gn file to the sources conditions map for targets in that
+ // file.
+ std::mutex sources_cache_lock_;
+ std::unordered_map<SourceFile, TargetSourcesMap> build_file_sources_cache_;
};
SuggestResult OutputSuggestions(const std::vector<const Target*>& all_targets,
@@ -355,15 +387,16 @@
// Resolves an input to a list of targets for suggestion.
// Specifically also decides whether it resolves to the public or private API
-// of the target.
-std::pair<std::vector<std::pair<const Target*, ApiScope>>, bool>
-ResolveSuggestionToTarget(const BuildSettings* build_settings,
- const std::vector<const Target*>& all_targets,
- const Label& current_toolchain,
- std::string_view input,
- bool must_be_file,
- TargetResolutionCache& cache,
- const Target* includer = nullptr);
+// of the target, and any enclosing conditions if it resolves to a conditional
+// source.
+std::pair<std::vector<ResolvedTarget>, bool> ResolveSuggestionToTarget(
+ const BuildSettings* build_settings,
+ const std::vector<const Target*>& all_targets,
+ const Label& current_toolchain,
+ std::string_view input,
+ bool must_be_file,
+ TargetResolutionCache& cache,
+ const Target* includer = nullptr);
// Resolves a vector of command line inputs and figures out the full set of
// things they resolve to.
diff --git a/src/gn/edit_command_unittest.cc b/src/gn/edit_command_unittest.cc
index 0f2f4ea..d61120d 100644
--- a/src/gn/edit_command_unittest.cc
+++ b/src/gn/edit_command_unittest.cc
@@ -10,9 +10,11 @@
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
+#include "gn/build_file_editor.h"
#include "gn/err.h"
#include "gn/filesystem_utils.h"
#include "gn/setup.h"
+#include "gn/string_atom.h"
#include "gn/test_with_scheduler.h"
#include "util/test/test.h"
@@ -965,4 +967,56 @@
)"));
}
+TEST_F(EditCommandTest, ExtractTargetSourcesConditions) {
+ base::ScopedTempDir temp_dir;
+ ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+ base::FilePath root_path = base::MakeAbsoluteFilePath(temp_dir.GetPath());
+
+ base::FilePath build_gn_path = root_path.AppendASCII("BUILD.gn");
+ std::string content = R"(
+source_set("my_target") {
+ sources = [ "uncond.cc" ]
+
+ if (is_win) {
+ sources += [ "win1.cc", "win2.cc" ]
+ if (nested) {
+ sources += [ "nested.cc" ]
+ }
+ } else if (is_linux) {
+ sources += [ "linux.cc" ]
+ } else {
+ sources += [ "generic.cc" ]
+ public = [ "generic.h" ]
+ }
+}
+)";
+ ASSERT_TRUE(WriteFile(build_gn_path, content, nullptr));
+
+ Setup setup;
+ setup.build_settings().SetRootPath(root_path);
+
+ auto build_file =
+ BuildFile::Create(&setup.build_settings(), SourceFile("//BUILD.gn"), {});
+ ASSERT_TRUE(build_file.has_value());
+
+ TargetSourcesMap target_sources = GetSourcesForTargets(*build_file);
+ ASSERT_TRUE(target_sources.contains(StringAtom("my_target")));
+
+ const auto& sources = target_sources[StringAtom("my_target")];
+
+ SourceConditionsMap expected = {
+ {SourceFile("//uncond.cc"), std::nullopt},
+ {SourceFile("//win1.cc"), StringAtom("if (is_win)")},
+ {SourceFile("//win2.cc"), StringAtom("if (is_win)")},
+ {SourceFile("//nested.cc"), StringAtom("if (is_win) -> if (nested)")},
+ {SourceFile("//linux.cc"),
+ StringAtom("if (is_win)'s else if (is_linux)")},
+ {SourceFile("//generic.cc"),
+ StringAtom("if (is_win)'s else (after else if)")},
+ {SourceFile("//generic.h"),
+ StringAtom("if (is_win)'s else (after else if)")},
+ };
+ EXPECT_EQ(expected, sources);
+}
+
} // namespace commands
diff --git a/src/gn/location.cc b/src/gn/location.cc
index cba44c8..1cfe4b7 100644
--- a/src/gn/location.cc
+++ b/src/gn/location.cc
@@ -9,6 +9,7 @@
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "gn/input_file.h"
+#include "gn/tokenizer.h"
Location::Location() = default;
@@ -67,3 +68,29 @@
return LocationRange(begin_ < other.begin_ ? begin_ : other.begin_,
end_ < other.end_ ? other.end_ : end_);
}
+
+std::string_view LocationRange::GetText() const {
+ if (is_null() || !begin_.file()) {
+ return {};
+ }
+ const std::string& contents = begin_.file()->contents();
+ size_t begin_line_off =
+ Tokenizer::ByteOffsetOfNthLine(contents, begin_.line_number());
+ if (begin_line_off == static_cast<size_t>(-1)) {
+ return {};
+ }
+ size_t begin_off = begin_line_off + (begin_.column_number() - 1);
+
+ size_t end_line_off =
+ Tokenizer::ByteOffsetOfNthLine(contents, end_.line_number());
+ if (end_line_off == static_cast<size_t>(-1)) {
+ return {};
+ }
+ size_t end_off = end_line_off + (end_.column_number() - 1);
+
+ if (begin_off > end_off || end_off > contents.size()) {
+ return {};
+ }
+
+ return std::string_view(contents).substr(begin_off, end_off - begin_off);
+}
diff --git a/src/gn/location.h b/src/gn/location.h
index 79af5dd..b6186a7 100644
--- a/src/gn/location.h
+++ b/src/gn/location.h
@@ -6,6 +6,7 @@
#define TOOLS_GN_LOCATION_H_
#include <string>
+#include <string_view>
class InputFile;
@@ -53,6 +54,10 @@
LocationRange Union(const LocationRange& other) const;
+ // Returns the text covered by this range in the source file, or an empty
+ // string_view if the range is null or invalid.
+ std::string_view GetText() const;
+
private:
Location begin_;
Location end_;
diff --git a/src/gn/tokenizer_unittest.cc b/src/gn/tokenizer_unittest.cc
index 6f6f0e0..e7e9c4a 100644
--- a/src/gn/tokenizer_unittest.cc
+++ b/src/gn/tokenizer_unittest.cc
@@ -162,6 +162,27 @@
EXPECT_EQ(2u, Tokenizer::ByteOffsetOfNthLine(input2, 2));
}
+TEST(Tokenizer, LocationRangeGetText) {
+ InputFile file(SourceFile("//test"));
+ file.SetContents("first line\nsecond line\nthird line");
+
+ // Entire first line
+ LocationRange first_line(Location(&file, 1, 1), Location(&file, 1, 11));
+ EXPECT_EQ(first_line.GetText(), "first line");
+
+ // Part of second line
+ LocationRange second_part(Location(&file, 2, 8), Location(&file, 2, 12));
+ EXPECT_EQ(second_part.GetText(), "line");
+
+ // Multi-line range
+ LocationRange multi_line(Location(&file, 1, 7), Location(&file, 2, 7));
+ EXPECT_EQ(multi_line.GetText(), "line\nsecond");
+
+ // Null range
+ LocationRange null_range;
+ EXPECT_TRUE(null_range.GetText().empty());
+}
+
TEST(Tokenizer, Comments) {
TokenExpectation fn[] = {
{Token::LINE_COMMENT, "# Stuff"},