Add `gn suggest` subcommand. This command will attempt to create suggestions to tell you what modifications to make to the build graph to fix errors. Bug: 500845363 Change-Id: I76212796fddb9586662e48bdca004e4b6a6a6964 Reviewed-on: https://gn-review.googlesource.com/c/gn/+/22140 Commit-Queue: Matt Stark <msta@google.com> Reviewed-by: Takuto Ikuta <tikuta@google.com>
diff --git a/build/gen.py b/build/gen.py index 44f92b9..432924e 100755 --- a/build/gen.py +++ b/build/gen.py
@@ -679,6 +679,7 @@ 'src/gn/command_outputs.cc', 'src/gn/command_path.cc', 'src/gn/command_refs.cc', + 'src/gn/command_suggest.cc', 'src/gn/commands.cc', 'src/gn/compile_commands_writer.cc', 'src/gn/rust_project_writer.cc', @@ -833,6 +834,7 @@ 'src/gn/bundle_data_unittest.cc', 'src/gn/c_include_iterator_unittest.cc', 'src/gn/command_format_unittest.cc', + 'src/gn/command_suggest_unittest.cc', 'src/gn/commands_unittest.cc', 'src/gn/compile_commands_writer_unittest.cc', 'src/gn/config_unittest.cc',
diff --git a/docs/reference.md b/docs/reference.md index f5781c6..1ca0d1c 100644 --- a/docs/reference.md +++ b/docs/reference.md
@@ -19,6 +19,7 @@ * [outputs: Which files a source/target make.](#cmd_outputs) * [path: Find paths between two targets.](#cmd_path) * [refs: Find stuff referencing a target or file.](#cmd_refs) + * [suggest: Suggest fixes to build graph based on includes.](#cmd_suggest) * [Target declarations](#targets) * [action: Declare a target that runs a script a single time.](#func_action) * [action_foreach: Declare a target that runs a script over a set of files.](#func_action_foreach) @@ -1394,6 +1395,23 @@ Display the executable file names of all test executables potentially affected by a change to the given file. ``` +### <a name="cmd_suggest"></a>**suggest**: Suggest fixes to build graph based on includes. [Back to Top](#gn-reference) + +``` + gn suggest <out_dir> includer1=included1 includer2=included2... + + Where each includer or included is either: + * A label + * A module name (usually the same as the label) + * A file path relative to the build directory + * An absolute file path (eg. "//foo/bar.txt") + + Eg. gn suggest out_dir path/to/target.cc=foo/bar.h + + Will print a suggestion like: + Request: path/to/target.cc wants to depend on foo/bar.h + Suggestion: add deps = [ "//foo:bar" ] to "//path/to:target" (defined in //path/to/BUILD.gn:1234) +``` ## <a name="targets"></a>Target declarations ### <a name="func_action"></a>**action**: Declare a target that runs a script a single time. [Back to Top](#gn-reference)
diff --git a/src/gn/command_suggest.cc b/src/gn/command_suggest.cc new file mode 100644 index 0000000..9550974 --- /dev/null +++ b/src/gn/command_suggest.cc
@@ -0,0 +1,408 @@ +// 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 <stddef.h> + +#include <vector> + +#include "base/files/file_util.h" +#include "base/strings/string_split.h" +#include "gn/commands.h" +#include "gn/filesystem_utils.h" +#include "gn/item.h" +#include "gn/setup.h" +#include "gn/standard_out.h" +#include "gn/target.h" + +namespace commands { + +const char kSuggest[] = "suggest"; +const char kSuggest_HelpShort[] = + "suggest: Suggest fixes to build graph based on includes."; +const char kSuggest_Help[] = + R"(suggest: Suggest fixes to build graph based on includes. + + gn suggest <out_dir> includer1=included1 includer2=included2... + + Where each includer or included is either: + * A label + * A module name (usually the same as the label) + * A file path relative to the build directory + * An absolute file path (eg. "//foo/bar.txt") + + Eg. gn suggest out_dir path/to/target.cc=foo/bar.h + + Will print a suggestion like: + Request: path/to/target.cc wants to depend on foo/bar.h + Suggestion: add deps = [ "//foo:bar" ] to "//path/to:target" (defined in //path/to/BUILD.gn:1234) +)"; + +constexpr std::string_view kPrivateSuffix = "_Private"; + +namespace { +// Determines whether a source file is in either the public or private API of a +// target. +std::optional<commands::ApiScope> DepKind(const Target* target, + const SourceFile& file) { + for (const auto& source : target->sources()) { + if (source == file) { + return target->all_headers_public() && + file.GetType() == SourceFile::SOURCE_H + ? commands::ApiScope::kPublic + : commands::ApiScope::kPrivate; + } + } + for (const auto& header : target->public_headers()) { + if (header == file) { + return commands::ApiScope::kPublic; + } + } + return std::nullopt; +} + +// 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, + std::vector<std::pair<const Target*, commands::ApiScope>>& results) { + for (const Target* target : all_targets) { + if (!current_toolchain || + target->label().GetToolchainLabel() == *current_toolchain) { + if (auto dep_kind = DepKind(target, file); dep_kind.has_value()) { + results.emplace_back(target, *dep_kind); + } + } + } + return !results.empty(); +} + +SourceFile ResolveFilePath(const BuildSettings* build_settings, + std::string_view input) { + if (input.starts_with("//")) { + SourceFile file = SourceFile(input); + if (base::PathExists(build_settings->GetFullPath(file))) { + return file; + } + return SourceFile(); + } + // Resolve relative to the output directory. + // This is because the user is most likely running this based on an error + // message from clang, which gives paths relative to the output directory to + // be unambiguous. + Err err; + SourceFile file = build_settings->build_dir().ResolveRelativeFile( + Value(nullptr, std::string(input)), &err); + if (!err.has_error() && base::PathExists(build_settings->GetFullPath(file))) { + return file; + } + return SourceFile(); +} + +constexpr auto kLabelLike = TextDecoration::DECORATION_GREEN; + +void OutputSuggestion(std::string_view message) { + OutputString("Suggestion: ", TextDecoration::DECORATION_BLUE); + OutputString(message); +} + +void OutputWarning(std::string_view message = "") { + OutputString("Warning: ", TextDecoration::DECORATION_YELLOW); + OutputString(message); +} + +void OutputError(std::string_view message = "") { + OutputString("Error: ", TextDecoration::DECORATION_RED); + OutputString(message); +} + +void OutputQuoted(std::string_view message) { + OutputString("\"", kLabelLike); + OutputString(message, kLabelLike); + OutputString("\"", kLabelLike); +} + +void OutputDefinition(const Target* target) { + OutputString(":", kLabelLike); + OutputString(target->label().name(), kLabelLike); + OutputString(" (defined at "); + OutputString(target->user_friendly_location().Describe(false), kLabelLike); + OutputString(")"); +} + +} // namespace + +// Resolves an input to a list of targets, and whether each are private. +// The input can be: +// * A module name for a target +// * A target label +// * A file path, which attempts to resolve to: +// * 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) { + auto sort_results = [](auto& vec) { + std::sort(vec.begin(), vec.end(), [](const auto& lhs, const auto& rhs) { + return lhs.first->label() < rhs.first->label(); + }); + }; + std::vector<std::pair<const Target*, commands::ApiScope>> results; + std::string_view module_name = input; + commands::ApiScope is_private = commands::ApiScope::kPublic; + if (module_name.ends_with(kPrivateSuffix)) { + is_private = commands::ApiScope::kPrivate; + module_name.remove_suffix(kPrivateSuffix.size()); + } + + // 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); + } + } + if (!results.empty()) { + sort_results(results); + return {results, true}; + } + + // If that doesn't work, try to resolve as an absolute target label. + if (input.starts_with("//") && input.find(':') != std::string_view::npos) { + Err err; + Label want; + Value input_value(nullptr, std::string(input)); + want = Label::Resolve(SourceDir("//"), build_settings->root_path_utf8(), + current_toolchain, input_value, &err); + if (!err.has_error()) { + for (const Target* target : all_targets) { + if (target->label() == want) { + results.emplace_back(target, is_private); + // We know each label corresponds to exactly one target, so we don't + // need to keep going. + return {results, true}; + } + } + } + } + + // If that doesn't work, try to resolve as a file path. + SourceFile file = ResolveFilePath(build_settings, input); + if (file.is_null()) { + return {results, false}; + } + + // If we see //foo(:toolchain) request bar.h, prefer //:bar(:toolchain) + // over other toolchains. + if (!AddToolchainSources(all_targets, ¤t_toolchain, file, results)) { + AddToolchainSources(all_targets, nullptr, file, results); + } + sort_results(results); + return {results, true}; +} + +bool OutputSuggestions(const std::vector<const Target*>& all_targets, + Setup* setup, + std::string_view includer_name, + std::string_view included_name) { + Label current_toolchain = setup->loader()->default_toolchain_label(); + auto OutputTarget = [¤t_toolchain](const Target* target) { + OutputString(target->label().GetUserVisibleName(current_toolchain), + kLabelLike); + }; + + auto OutputInsertionHint = [&](std::string_view key, std::string_view value, + const Target* target) { + OutputSuggestion("Add "); + OutputString(key); + OutputString(" = [ "); + OutputQuoted(value); + OutputString(" ] to "); + OutputDefinition(target); + if (current_toolchain != setup->loader()->default_toolchain_label()) { + OutputString(" for toolchain "); + OutputString( + target->label().GetToolchainLabel().GetUserVisibleName(false), + kLabelLike); + } + OutputString("\n"); + }; + + auto ResolveSuggestion = [&](std::string_view value) { + const auto& [targets, ok] = ResolveSuggestionToTarget( + &setup->build_settings(), all_targets, current_toolchain, value); + if (!ok) { + OutputError(); + if (value.starts_with("//")) { + OutputString("Could not find target or file "); + OutputQuoted(value); + } else { + OutputString("Unable to find "); + OutputQuoted(value); + OutputString(" in either the output or source root directories\n"); + } + } + return std::make_pair(targets, ok); + }; + + const auto& [includer_targets, includer_ok] = + ResolveSuggestion(includer_name); + if (!includer_ok) + return false; + + if (includer_targets.empty()) { + OutputError(); + OutputQuoted(includer_name); + OutputString(" did not resolve to any targets\n"); + return false; + } else if (includer_targets.size() > 1) { + OutputError(); + OutputQuoted(includer_name); + OutputString(" resolved to multiple targets\n"); + for (const auto& [target, is_private] : includer_targets) { + OutputString("* "); + OutputTarget(target); + OutputString("\n"); + } + return false; + } + const auto& [includer, dep_kind] = includer_targets.front(); + current_toolchain = includer->label().GetToolchainLabel(); + + const char* dep_field = + (dep_kind == commands::ApiScope::kPrivate) ? "deps" : "public_deps"; + + const auto& [targets, ok] = ResolveSuggestion(included_name); + if (!ok) + return false; + + // We've passed the errors phase. At this point, everything is valid input. + // Includer is a single target, and included is a valid target, or a file + // that exists on disk. + + if (targets.empty()) { + OutputQuoted(included_name); + OutputString(" is not in the headers of any targets.\n"); + OutputSuggestion("Add "); + OutputQuoted(included_name); + OutputString(" to a target's public headers"); + return true; + } + + std::set<Label> labels_without_toolchain; + for (const auto& [target, _] : targets) { + labels_without_toolchain.insert(target->label().GetWithNoToolchain()); + } + if (labels_without_toolchain.size() == 1 && + targets.front().first->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: + // //:foo(:toolchain1) including bar.h -> //:bar(:toolchain2), + // //:bar(:toolchain3) + OutputQuoted(included_name); + OutputString(" is defined in "); + OutputString(labels_without_toolchain.begin()->GetUserVisibleName(false), + kLabelLike); + OutputString(", but not in the toolchain "); + OutputString(current_toolchain.GetUserVisibleName(false), kLabelLike); + OutputString("\n"); + OutputInsertionHint("public", included_name, targets.front().first); + return true; + } + + if (targets.size() > 1) { + OutputWarning(); + OutputQuoted(included_name); + OutputString(" is ambiguous because it belongs to multiple targets:\n"); + for (const auto& [target, _] : targets) { + OutputString("* "); + OutputTarget(target); + OutputString("\n"); + } + OutputSuggestion( + "Create a source_set target for the common headers and sources and " + "have all of the above targets depend on that."); + OutputInsertionHint(dep_field, "$NEW_SOURCE_SET", includer); + return true; + } + + const auto& [included, included_dep_kind] = targets.front(); + if (included_dep_kind == commands::ApiScope::kPrivate) { + OutputWarning(); + OutputQuoted(included_name); + OutputString(" is in the private API of "); + OutputTarget(included); + OutputSuggestion("Move "); + OutputQuoted(included_name); + OutputString(" from `sources` to `public` in "); + OutputDefinition(included); + } + + // TODO: There are a bunch of optimizations we can perform here to make better + // suggestions. They may be considered in the future. Some initial thoughts + // include: + // * Check the visibility of includer -> included + // * If it is not visible: + // * Find a group target that exposes included's headers + // * Fall back to suggesting adding visibility + // * Check if included transitively depends on includer. Suggest ways to break + // the loop. + + // Note: if we have a toolchain mismatch, we already returned, so the + // toolchains must match. + OutputInsertionHint( + dep_field, + // Output a relative label if possible. + included->label().dir() == includer->label().dir() + ? ":" + included->label().name() + : included->label().GetUserVisibleName(current_toolchain), + includer); + return true; +} + +int RunSuggest(const std::vector<std::string>& args) { + if (args.size() <= 1) { + OutputError("gn suggest requires arguments. See \"gn help suggest\"\n"); + return 1; + } + + // Deliberately leaked to avoid expensive process teardown. + Setup* setup = new Setup; + if (!setup->DoSetup(args[0], false) || !setup->Run()) + return 1; + + std::vector<const Target*> all_targets = + setup->builder().GetAllResolvedTargets(); + + bool success = true; + for (size_t i = 1; i < args.size(); i++) { + if (i != 1) { + OutputString("\n"); + } + std::vector<std::string_view> pair = base::SplitStringPiece( + args[i], "=", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + if (pair.size() != 2) { + OutputError("Invalid pair: " + args[i] + "\n"); + return 1; + } + const auto& includer = pair[0]; + const auto& included = pair[1]; + + OutputString("Request: ", TextDecoration::DECORATION_MAGENTA); + OutputQuoted(includer); + OutputString(" wants to depend on "); + OutputQuoted(included); + OutputString(":\n"); + + success &= OutputSuggestions(all_targets, setup, includer, included); + } + + return success ? 0 : 1; +} + +} // namespace commands
diff --git a/src/gn/command_suggest_unittest.cc b/src/gn/command_suggest_unittest.cc new file mode 100644 index 0000000..e752561 --- /dev/null +++ b/src/gn/command_suggest_unittest.cc
@@ -0,0 +1,218 @@ +// 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 <string> +#include <string_view> +#include <vector> + +#include "base/command_line.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "gn/commands.h" +#include "gn/setup.h" +#include "gn/switches.h" +#include "gn/target.h" +#include "gn/test_with_scheduler.h" +#include "gn/test_with_scope.h" +#include "util/test/test.h" + +TEST(Suggest, ResolveModuleName) { + TestWithScope setup_scope; + SourceDir current_dir("//"); + Label default_toolchain(SourceDir("//toolchain/"), "default"); + Err err; + + Target target(setup_scope.settings(), Label(SourceDir("//foo/"), "bar")); + target.set_module_name("my_module"); + + std::vector<const Target*> all_targets = {&target}; + + { + auto [results, ok] = commands::ResolveSuggestionToTarget( + setup_scope.build_settings(), all_targets, default_toolchain, + "my_module"); + std::vector<std::pair<const Target*, commands::ApiScope>> expected = { + {&target, commands::ApiScope::kPublic}}; + EXPECT_EQ(expected, results); + EXPECT_TRUE(ok); + } + + // Test resolving module name "my_module_Private" + { + auto [results, ok] = commands::ResolveSuggestionToTarget( + setup_scope.build_settings(), all_targets, default_toolchain, + "my_module_Private"); + std::vector<std::pair<const Target*, commands::ApiScope>> expected = { + {&target, commands::ApiScope::kPrivate}}; + EXPECT_EQ(expected, results); + EXPECT_TRUE(ok); + } +} + +TEST(Suggest, ResolveTargetName) { + TestWithScope setup_scope; + SourceDir current_dir("//"); + Label default_toolchain = setup_scope.toolchain()->label(); + Err err; + + Target target( + setup_scope.settings(), + Label(SourceDir("//"), "hello", setup_scope.toolchain()->label().dir(), + setup_scope.toolchain()->label().name())); + Target target_gcc( + setup_scope.settings(), + Label(SourceDir("//"), "hello", SourceDir("//build/toolchain/"), "gcc")); + std::vector<const Target*> all_targets = {&target, &target_gcc}; + + // Test resolving "//:hello" + auto [results_label, ok_label] = commands::ResolveSuggestionToTarget( + setup_scope.build_settings(), all_targets, + setup_scope.toolchain()->label(), "//:hello"); + + std::vector<std::pair<const Target*, commands::ApiScope>> expected_label = { + {&target, commands::ApiScope::kPublic}}; + EXPECT_EQ(expected_label, results_label); + EXPECT_TRUE(ok_label); + + // Test resolving "//:hello(//build/toolchain:gcc)" + auto [results_toolchain, ok_toolchain] = commands::ResolveSuggestionToTarget( + setup_scope.build_settings(), all_targets, default_toolchain, + "//:hello(//build/toolchain:gcc)"); + + std::vector<std::pair<const Target*, commands::ApiScope>> expected_toolchain = + {{&target_gcc, commands::ApiScope::kPublic}}; + EXPECT_EQ(expected_toolchain, results_toolchain); + EXPECT_TRUE(ok_toolchain); +} + +TEST(Suggest, ResolveFileName) { + TestWithScope setup_scope; + SourceDir current_dir("//"); + Label default_toolchain = setup_scope.toolchain()->label(); + Label current_toolchain(SourceDir("//build/toolchain/"), "gcc"); + Label secondary_toolchain(SourceDir("//build/toolchain/"), "clang"); + Err err; + + // Follow standard practice to create temporary directories in tests. + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + base::FilePath root_dir = temp_dir.GetPath(); + setup_scope.build_settings()->SetRootPath(root_dir); + + base::WriteFile(root_dir.AppendASCII("public.h"), "", 0); + base::WriteFile(root_dir.AppendASCII("private.h"), "", 0); + base::WriteFile(root_dir.AppendASCII("implicit_public.h"), "", 0); + base::WriteFile(root_dir.AppendASCII("no_target.h"), "", 0); + base::WriteFile(root_dir.AppendASCII("simple.h"), "", 0); + base::WriteFile(root_dir.AppendASCII("default_toolchain.h"), "", 0); + base::WriteFile(root_dir.AppendASCII("secondary_toolchain.h"), "", 0); + + Target explicit_target( + setup_scope.settings(), + Label(SourceDir("//"), "explicit", current_toolchain.dir(), + current_toolchain.name())); + explicit_target.set_all_headers_public(false); + explicit_target.sources().push_back(SourceFile("//private.h")); + explicit_target.public_headers().push_back(SourceFile("//public.h")); + explicit_target.public_headers().push_back( + SourceFile("//nonexistent_file.h")); + + Target implicit_target( + setup_scope.settings(), + Label(SourceDir("//"), "implicit", default_toolchain.dir(), + default_toolchain.name())); + implicit_target.set_all_headers_public(true); + implicit_target.sources().push_back(SourceFile("//implicit_public.h")); + implicit_target.sources().push_back(SourceFile("//private.cc")); + + Target simple_default( + setup_scope.settings(), + Label(SourceDir("//"), "simple", default_toolchain.dir(), + default_toolchain.name())); + simple_default.public_headers().push_back(SourceFile("//public.h")); + simple_default.public_headers().push_back( + SourceFile("//default_toolchain.h")); + + Target simple_secondary( + setup_scope.settings(), + Label(SourceDir("//"), "simple", secondary_toolchain.dir(), + secondary_toolchain.name())); + simple_secondary.public_headers().push_back(SourceFile("//public.h")); + simple_secondary.public_headers().push_back( + SourceFile("//default_toolchain.h")); + simple_secondary.public_headers().push_back( + SourceFile("//secondary_toolchain.h")); + + std::vector<const Target*> all_targets = {&explicit_target, &implicit_target, + &simple_default, &simple_secondary}; + + { + auto [results, ok] = commands::ResolveSuggestionToTarget( + setup_scope.build_settings(), all_targets, current_toolchain, + "//public.h"); + std::vector<std::pair<const Target*, commands::ApiScope>> expected = { + {&explicit_target, commands::ApiScope::kPublic}}; + EXPECT_TRUE(ok); + EXPECT_EQ(expected, results); + } + + { + auto [results, ok] = commands::ResolveSuggestionToTarget( + setup_scope.build_settings(), all_targets, current_toolchain, + "../../private.h"); + std::vector<std::pair<const Target*, commands::ApiScope>> expected = { + {&explicit_target, commands::ApiScope::kPrivate}}; + EXPECT_TRUE(ok); + EXPECT_EQ(expected, results); + } + + { + auto [results, ok] = commands::ResolveSuggestionToTarget( + setup_scope.build_settings(), all_targets, current_toolchain, + "//implicit_public.h"); + std::vector<std::pair<const Target*, commands::ApiScope>> expected = { + {&implicit_target, commands::ApiScope::kPublic}}; + EXPECT_TRUE(ok); + EXPECT_EQ(expected, results); + } + + { + auto [results, ok] = commands::ResolveSuggestionToTarget( + setup_scope.build_settings(), all_targets, current_toolchain, + "nonexistent_file.h"); + EXPECT_FALSE(ok); + } + + { + auto [results, ok] = commands::ResolveSuggestionToTarget( + setup_scope.build_settings(), all_targets, current_toolchain, + "//no_target.h"); + std::vector<std::pair<const Target*, commands::ApiScope>> expected_targets; + EXPECT_TRUE(ok); + EXPECT_EQ(expected_targets, results); + } + + { + auto [results, ok] = commands::ResolveSuggestionToTarget( + setup_scope.build_settings(), all_targets, current_toolchain, + "//default_toolchain.h"); + std::vector<std::pair<const Target*, commands::ApiScope>> expected_targets = + { + {&simple_secondary, commands::ApiScope::kPublic}, + {&simple_default, commands::ApiScope::kPublic}, + }; + EXPECT_TRUE(ok); + EXPECT_EQ(expected_targets, results); + } + + { + auto [results, ok] = commands::ResolveSuggestionToTarget( + setup_scope.build_settings(), all_targets, current_toolchain, + "//secondary_toolchain.h"); + std::vector<std::pair<const Target*, commands::ApiScope>> expected_targets = + {{{&simple_secondary, commands::ApiScope::kPublic}}}; + EXPECT_TRUE(ok); + EXPECT_EQ(expected_targets, results); + } +}
diff --git a/src/gn/commands.cc b/src/gn/commands.cc index dbf2b48..c37bc6a 100644 --- a/src/gn/commands.cc +++ b/src/gn/commands.cc
@@ -390,6 +390,7 @@ INSERT_COMMAND(Outputs) INSERT_COMMAND(Path) INSERT_COMMAND(Refs) + INSERT_COMMAND(Suggest) INSERT_COMMAND(CleanStale) #undef INSERT_COMMAND
diff --git a/src/gn/commands.h b/src/gn/commands.h index 702bf0c..fcc5bd8 100644 --- a/src/gn/commands.h +++ b/src/gn/commands.h
@@ -98,6 +98,11 @@ extern const char kRefs_Help[]; int RunRefs(const std::vector<std::string>& args); +extern const char kSuggest[]; +extern const char kSuggest_HelpShort[]; +extern const char kSuggest_Help[]; +int RunSuggest(const std::vector<std::string>& args); + extern const char kCleanStale[]; extern const char kCleanStale_HelpShort[]; extern const char kCleanStale_Help[]; @@ -257,6 +262,20 @@ Setup* setup, const std::string& label_string); +enum class ApiScope { + kPublic, + kPrivate, +}; + +// 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); + // Resolves a vector of command line inputs and figures out the full set of // things they resolve to. //
diff --git a/src/gn/standard_out.cc b/src/gn/standard_out.cc index ec8efde..22761ea 100644 --- a/src/gn/standard_out.cc +++ b/src/gn/standard_out.cc
@@ -104,7 +104,7 @@ #if defined(OS_WIN) -void OutputString(const std::string& output, +void OutputString(std::string_view output, TextDecoration dec, HtmlEscaping escaping) { EnsureInitialized(); @@ -141,7 +141,7 @@ } } - std::string tmpstr = output; + std::string tmpstr = std::string(output); if (is_markdown && dec == DECORATION_YELLOW) { // https://code.google.com/p/gitiles/issues/detail?id=77 // Gitiles will replace "--" with an em dash in non-code text. @@ -167,7 +167,7 @@ #else -void OutputString(const std::string& output, +void OutputString(std::string_view output, TextDecoration dec, HtmlEscaping escaping) { EnsureInitialized(); @@ -198,7 +198,7 @@ } } - std::string tmpstr = output; + std::string tmpstr = std::string(output); if (is_markdown && dec == DECORATION_YELLOW) { // https://code.google.com/p/gitiles/issues/detail?id=77 // Gitiles will replace "--" with an em dash in non-code text.
diff --git a/src/gn/standard_out.h b/src/gn/standard_out.h index b737fa1..9f43451 100644 --- a/src/gn/standard_out.h +++ b/src/gn/standard_out.h
@@ -25,7 +25,7 @@ DEFAULT_ESCAPING, }; -void OutputString(const std::string& output, +void OutputString(std::string_view output, TextDecoration dec = DECORATION_NONE, HtmlEscaping = DEFAULT_ESCAPING);
diff --git a/src/gn/target.cc b/src/gn/target.cc index f170c5d..91cb72c 100644 --- a/src/gn/target.cc +++ b/src/gn/target.cc
@@ -413,6 +413,12 @@ Target::~Target() = default; +Location Target::user_friendly_location() const { + if (!user_friendly_location_.is_null()) + return user_friendly_location_; + return defined_from()->GetRange().begin(); +} + // A technical note on accessors defined below: Using a static global // constant is much faster at runtime than using a static local one. //
diff --git a/src/gn/target.h b/src/gn/target.h index 83b1e37..347ca6b 100644 --- a/src/gn/target.h +++ b/src/gn/target.h
@@ -452,6 +452,13 @@ module_name_ = std::move(module_name); } + // Similar to defined_from(), but for targets created via templates, returns + // the invoker of the template rather than the template definition. + Location user_friendly_location() const; + void set_user_friendly_location(Location location) { + user_friendly_location_ = location; + } + // Computes and returns the outputs of this target expressed as SourceFiles. // // For binary target this depends on the tool for this target so the toolchain @@ -541,6 +548,7 @@ std::string module_name_; ModuleType module_type_; + Location user_friendly_location_; // Only filled if the module type is GENERATED_* SourceFile generated_modulemap_file_; // For performance reasons we cache private_modulemap_file.
diff --git a/src/gn/target_generator.cc b/src/gn/target_generator.cc index 7529b2e..bde52e4 100644 --- a/src/gn/target_generator.cc +++ b/src/gn/target_generator.cc
@@ -42,6 +42,14 @@ TargetGenerator::~TargetGenerator() = default; void TargetGenerator::Run() { + // If the target is defined from a template, define_from is probably + // not particularly helpful for the user. So instead, we record the + // location of the template invocation in the target. + const auto& entries = scope_->GetTemplateInvocationEntries(); + if (!entries.empty()) { + target_->set_user_friendly_location(entries.front().location); + } + // All target types use these. if (!FillDependentConfigs()) return;