Improve heuristic to disambiguate recommended dependency to add.

If it still is ambiguous after applying visibility rules, we
disambiguate by label.

Change-Id: I1a36609a24091cc4780e166532f211f26a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/26420
Commit-Queue: Matt Stark <msta@google.com>
Reviewed-by: Takuto Ikuta <tikuta@google.com>
diff --git a/src/gn/command_suggest.cc b/src/gn/command_suggest.cc
index 6a13507..c6ff2f8 100644
--- a/src/gn/command_suggest.cc
+++ b/src/gn/command_suggest.cc
@@ -918,6 +918,50 @@
     visible_candidates = nonpublic_candidates;
   }
 
+  if (visible_candidates.size() > 1) {
+    auto ClosenessHeuristic =
+        [&](const Target* candidate) -> std::pair<int, size_t> {
+      // Consider the following example:
+      // * We attempted to include a file from //absl:strings.
+      // * //absl:strings is private, but is part of a group //absl:absl.
+      // * //angle:absl re-exports //absl:absl. It should ideally only be
+      //   visible to //angle/..., but may not be.
+      //
+      // Then:
+      // * If we include it from //angle:foo or //angle/subdir:foo, we should
+      //   get //angle:absl.
+      // * If we include it from outside, we should get the canonical one
+      //   (//absl:absl).
+      std::string_view cand_dir = candidate->label().dir().value();
+      std::string_view includer_dir = includer->label().dir().value();
+      std::string_view included_dir = included->label().dir().value();
+      // SourceDir always ends with "/", so string starts_with is ok.
+      if (includer_dir.starts_with(cand_dir)) {
+        // Prefer the more specific option.
+        return {2, cand_dir.size()};
+      } else if (included_dir.starts_with(cand_dir)) {
+        // If no option is more specific, we should prefer the "canonical" one.
+        // We assume it to be canonical if it's defined in the tree of the
+        // original target. (Eg. //absl re-exporting //absl/subdir:foo)
+        //
+        // We intentionally make no decision on whether //absl:exporter or
+        // //absl/subdir:exporter would be "more canonical" - we can change this
+        // later if we'd like, but for now we'd treat this as ambiguous.
+        return {1, 0};
+      }
+      return {0, 0};
+    };
+
+    std::ranges::stable_sort(
+        visible_candidates, [&](const Target* lhs, const Target* rhs) {
+          return ClosenessHeuristic(lhs) > ClosenessHeuristic(rhs);
+        });
+    if (ClosenessHeuristic(visible_candidates[0]) !=
+        ClosenessHeuristic(visible_candidates[1])) {
+      visible_candidates.resize(1);
+    }
+  }
+
   if (visible_candidates.size() == 1) {
     OutputDepSuggestion(visible_candidates);
   } else if (visible_candidates.size() > 1) {
diff --git a/src/gn/command_suggest_unittest.cc b/src/gn/command_suggest_unittest.cc
index 3f2833a..6cee00c 100644
--- a/src/gn/command_suggest_unittest.cc
+++ b/src/gn/command_suggest_unittest.cc
@@ -385,11 +385,10 @@
   };
 
   auto create_target = [&](std::string_view name, Target::OutputType type,
-                           auto fn) {
+                           auto fn, SourceDir dir = SourceDir("//")) {
     auto target = std::make_unique<Target>(
         setup_scope.settings(),
-        Label(SourceDir("//"), name, default_toolchain.dir(),
-              default_toolchain.name()));
+        Label(dir, name, default_toolchain.dir(), default_toolchain.name()));
     target->set_output_type(type);
     target->SetToolchain(setup_scope.toolchain());
     target->set_user_friendly_location(dummy_loc);
@@ -399,7 +398,7 @@
       target->set_module_type(module_type);
       target->set_module_name(std::string(name));
       target->public_headers().push_back(
-          SourceFile("//" + std::string(name) + ".h"));
+          SourceFile(dir.value() + std::string(name) + ".h"));
     }
     fn(target.get());
     Err err;
@@ -410,18 +409,21 @@
 
   auto includer = create_target("includer", Target::GROUP, [](Target*) {});
 
-  auto run_suggest = [&](std::string_view want) {
+  auto run_suggest_for = [&](std::string_view from, std::string_view want) {
     std::string output;
     auto collect = [&](std::string_view s, TextDecoration, HtmlEscaping) {
       output.append(s);
     };
     commands::TargetResolutionCache cache;
     commands::OutputSuggestions(all_targets, setup_scope.build_settings(),
-                                default_toolchain, "//:includer", want, collect,
-                                cache);
+                                default_toolchain, from, want, collect, cache);
     return output;
   };
 
+  auto run_suggest = [&](std::string_view want) {
+    return run_suggest_for("//:includer", want);
+  };
+
   auto visible = create_target("visible", Target::SOURCE_SET,
                                [&](Target* t) { t->visibility().SetPublic(); });
   auto visible_group =
@@ -578,6 +580,34 @@
       "(defined at //BUILD.gn:1)\n"
       "  (`gn edit \"add public_deps :private_target\" //:includer`)\n",
       run_suggest("private_target_Private"));
+
+  auto pkg_target = create_target(
+      "pkg_target", Target::SOURCE_SET, [](Target* t) {}, SourceDir("//pkg/"));
+  auto pkg_includer = create_target(
+      "pkg_includer", Target::GROUP, [](Target*) {}, SourceDir("//pkg/sub/"));
+  auto other_exposer = create_target(
+      "other_exposer", Target::GROUP,
+      [&](Target* t) {
+        t->public_deps().push_back(LabelTargetPair(pkg_target.get()));
+        t->visibility().SetPublic();
+      },
+      SourceDir("//other/"));
+  auto parent_exposer = create_target(
+      "parent_exposer", Target::GROUP,
+      [&](Target* t) {
+        t->public_deps().push_back(LabelTargetPair(pkg_target.get()));
+        t->visibility().SetPublic();
+      },
+      SourceDir("//pkg/"));
+
+  // Disambiguate by package closeness: parent_exposer is in //pkg/, which is an
+  // ancestor of //pkg/sub/, so it is preferred over other_exposer in //other/.
+  EXPECT_EQ(
+      "Suggestion: Add public_deps = [ \"//pkg:parent_exposer\" ] to "
+      ":pkg_includer (defined at //BUILD.gn:1)\n"
+      "  (`gn edit \"add public_deps //pkg:parent_exposer\" "
+      "//pkg/sub:pkg_includer`)\n",
+      run_suggest_for("//pkg/sub:pkg_includer", pkg_target->module_name()));
 }
 
 TEST_F(SuggestTest, ApplyValidSuggestion) {