Resolve suggest inputs relative to target include directories.

When a file path passed to `gn suggest` cannot be resolved relative to
the output or source root directories, fall back to treating it as a
#include and attempt to resolve it relative to the include directories
of the includer target.

Bug: 500845363
Change-Id: If1bcb31e18eda51842f4d7b678fe81c56a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/23000
Commit-Queue: Matt Stark <msta@google.com>
Reviewed-by: Takuto Ikuta <tikuta@google.com>
diff --git a/docs/reference.md b/docs/reference.md
index 3012a11..5a509df 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -1405,6 +1405,7 @@
   * 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")
+  * A file path relative to the include directories of the includer target (treated as a #include)
 
   Eg. gn suggest out_dir path/to/target.cc=foo/bar.h
 
diff --git a/src/gn/command_suggest.cc b/src/gn/command_suggest.cc
index f9f9f15..b7cfc4d 100644
--- a/src/gn/command_suggest.cc
+++ b/src/gn/command_suggest.cc
@@ -15,6 +15,7 @@
 #include "base/files/file_util.h"
 #include "base/strings/string_split.h"
 #include "gn/commands.h"
+#include "gn/config_values_extractors.h"
 #include "gn/filesystem_utils.h"
 #include "gn/item.h"
 #include "gn/setup.h"
@@ -36,6 +37,7 @@
   * 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")
+  * A file path relative to the include directories of the includer target (treated as a #include)
 
   Eg. gn suggest out_dir path/to/target.cc=foo/bar.h
 
@@ -116,7 +118,8 @@
 
 SourceFile ResolveFilePath(const BuildSettings* build_settings,
                            const std::vector<const Target*>& all_targets,
-                           std::string_view input) {
+                           std::string_view input,
+                           const Target* includer = nullptr) {
   if (input.starts_with("//")) {
     SourceFile file = SourceFile(input);
     if (FileExists(all_targets, file, build_settings)) {
@@ -124,16 +127,34 @@
     }
     return SourceFile();
   }
+  Value input_value(nullptr, std::string(input));
+
   // 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);
+  SourceFile file =
+      build_settings->build_dir().ResolveRelativeFile(input_value, &err);
   if (!err.has_error() && FileExists(all_targets, file, build_settings)) {
     return file;
   }
+  // If we are unable to resolve the file, we should treat it as a #include.
+  // Thus, iterate through the include directories of the includer target to
+  // attempt to find a source file that exists relative to those include dirs.
+  if (includer) {
+    for (ConfigValuesIterator iter(includer); !iter.done(); iter.Next()) {
+      for (const SourceDir& dir : iter.cur().include_dirs()) {
+        Err resolve_err;
+        SourceFile resolved_file =
+            dir.ResolveRelativeFile(input_value, &resolve_err);
+        if (!resolve_err.has_error() &&
+            FileExists(all_targets, resolved_file, build_settings)) {
+          return resolved_file;
+        }
+      }
+    }
+  }
   return SourceFile();
 }
 
@@ -228,7 +249,8 @@
 ResolveSuggestionToTarget(const BuildSettings* build_settings,
                           const std::vector<const Target*>& all_targets,
                           const Label& current_toolchain,
-                          std::string_view input) {
+                          std::string_view input,
+                          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();
@@ -273,7 +295,8 @@
   }
 
   // If that doesn't work, try to resolve as a file path.
-  SourceFile file = ResolveFilePath(build_settings, all_targets, input);
+  SourceFile file =
+      ResolveFilePath(build_settings, all_targets, input, includer);
   if (file.is_null()) {
     return {results, false};
   }
@@ -378,9 +401,10 @@
     }
   };
 
-  auto ResolveSuggestion = [&](std::string_view value) {
+  auto ResolveSuggestion = [&](std::string_view value,
+                               const Target* target_context = nullptr) {
     const auto& [targets, ok] = ResolveSuggestionToTarget(
-        build_settings, all_targets, current_toolchain, value);
+        build_settings, all_targets, current_toolchain, value, target_context);
     if (!ok) {
       StartError();
       if (value.starts_with("//")) {
@@ -389,7 +413,11 @@
       } else {
         OutputString("Unable to find ");
         OutputQuoted(value);
-        OutputString(" in either the output or source root directories\n");
+        if (target_context) {
+          OutputString(" in the output, source root, or include directories\n");
+        } else {
+          OutputString(" in either the output or source root directories\n");
+        }
       }
     }
     return std::make_pair(targets, ok);
@@ -422,7 +450,7 @@
   const char* dep_field =
       (dep_kind == commands::ApiScope::kPrivate) ? "deps" : "public_deps";
 
-  const auto& [targets, ok] = ResolveSuggestion(included_name);
+  const auto& [targets, ok] = ResolveSuggestion(included_name, includer);
   if (!ok)
     return false;
 
diff --git a/src/gn/command_suggest_unittest.cc b/src/gn/command_suggest_unittest.cc
index 28131b2..1d02934 100644
--- a/src/gn/command_suggest_unittest.cc
+++ b/src/gn/command_suggest_unittest.cc
@@ -109,6 +109,9 @@
   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);
+  base::FilePath inc_dir = root_dir.AppendASCII("include_dir");
+  ASSERT_TRUE(base::CreateDirectory(inc_dir));
+  base::WriteFile(inc_dir.AppendASCII("my_header.h"), "", 0);
 
   Target explicit_target(
       setup_scope.settings(),
@@ -156,6 +159,16 @@
   Err resolve_err;
   ASSERT_TRUE(generated.OnResolvedWithoutChecks(&resolve_err));
 
+  Target included_target(
+      setup_scope.settings(),
+      Label(SourceDir("//"), "included_target", current_toolchain.dir(),
+            current_toolchain.name()));
+  included_target.set_output_type(Target::SOURCE_SET);
+  included_target.SetToolchain(setup_scope.toolchain());
+  included_target.set_all_headers_public(true);
+  included_target.sources().push_back(SourceFile("//include_dir/my_header.h"));
+  ASSERT_TRUE(included_target.OnResolvedWithoutChecks(&resolve_err));
+
   Target consumer(setup_scope.settings(),
                   Label(SourceDir("//"), "consumer", current_toolchain.dir(),
                         current_toolchain.name()));
@@ -164,11 +177,13 @@
   consumer.set_all_headers_public(true);
   consumer.public_headers().push_back(
       SourceFile("//out/Debug/generated_file.h"));
+  consumer.config_values().include_dirs().push_back(
+      SourceDir("//include_dir/"));
   ASSERT_TRUE(consumer.OnResolvedWithoutChecks(&resolve_err));
 
   std::vector<const Target*> all_targets = {&explicit_target, &implicit_target,
-                                            &simple_default, &simple_secondary,
-                                            &generated};
+                                            &simple_default,  &simple_secondary,
+                                            &generated,       &included_target};
 
   {
     auto [results, ok] = commands::ResolveSuggestionToTarget(
@@ -259,6 +274,16 @@
     EXPECT_TRUE(ok);
     EXPECT_EQ(expected_targets, results);
   }
+
+  {
+    auto [results, ok] = commands::ResolveSuggestionToTarget(
+        setup_scope.build_settings(), all_targets, current_toolchain,
+        "my_header.h", &consumer);
+    EXPECT_TRUE(ok);
+    std::vector<std::pair<const Target*, commands::ApiScope>> expected_targets =
+        {{{&included_target, commands::ApiScope::kPublic}}};
+    EXPECT_EQ(expected_targets, results);
+  }
 }
 
 TEST(Suggest, OutputSuggestions) {
diff --git a/src/gn/commands.h b/src/gn/commands.h
index c25ba85..d5b07ab 100644
--- a/src/gn/commands.h
+++ b/src/gn/commands.h
@@ -286,7 +286,8 @@
 ResolveSuggestionToTarget(const BuildSettings* build_settings,
                           const std::vector<const Target*>& all_targets,
                           const Label& current_toolchain,
-                          std::string_view input);
+                          std::string_view input,
+                          const Target* includer = nullptr);
 
 // Resolves a vector of command line inputs and figures out the full set of
 // things they resolve to.