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.&nbsp;[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.&nbsp;[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, &current_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 = [&current_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;