Refactor `OutputSuggestions` to make it testable.

Bug: 500845363
Change-Id: I11df1e31cc58e125b372b0bbef6137886a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/22720
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 0427e49..54110fe 100644
--- a/src/gn/command_suggest.cc
+++ b/src/gn/command_suggest.cc
@@ -4,6 +4,7 @@
 
 #include <stddef.h>
 
+#include <functional>
 #include <vector>
 
 #include "base/files/file_util.h"
@@ -100,38 +101,6 @@
   }
   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.
@@ -206,24 +175,58 @@
 }
 
 bool OutputSuggestions(const std::vector<const Target*>& all_targets,
-                       Setup* setup,
+                       const BuildSettings* build_settings,
+                       const Label& default_toolchain,
                        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) {
+                       std::string_view included_name,
+                       OutputStringFunc output_fn) {
+  auto OutputString =
+      [&](std::string_view str, TextDecoration dec = DECORATION_NONE,
+          HtmlEscaping esc = DEFAULT_ESCAPING) { output_fn(str, dec, esc); };
+
+  constexpr auto kLabelLike = TextDecoration::DECORATION_GREEN;
+
+  auto StartSuggestion = [&]() {
+    OutputString("Suggestion: ", TextDecoration::DECORATION_BLUE);
+  };
+  auto StartWarning = [&]() {
+    OutputString("Warning: ", TextDecoration::DECORATION_YELLOW);
+  };
+  auto StartError = [&]() {
+    OutputString("Error: ", TextDecoration::DECORATION_RED);
+  };
+
+  auto OutputQuoted = [&](std::string_view message) {
+    OutputString("\"", kLabelLike);
+    OutputString(message, kLabelLike);
+    OutputString("\"", kLabelLike);
+  };
+
+  auto OutputDefinition = [&](const Target* target) {
+    OutputString(":", kLabelLike);
+    OutputString(target->label().name(), kLabelLike);
+    OutputString(" (defined at ");
+    OutputString(target->user_friendly_location().Describe(false), kLabelLike);
+    OutputString(")");
+  };
+
+  Label current_toolchain = default_toolchain;
+  auto OutputTarget = [&current_toolchain,
+                       &OutputString](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 ");
+    StartSuggestion();
+    OutputString("Add ");
     OutputString(key);
     OutputString(" = [ ");
     OutputQuoted(value);
     OutputString(" ] to ");
     OutputDefinition(target);
-    if (current_toolchain != setup->loader()->default_toolchain_label()) {
+    if (current_toolchain != default_toolchain) {
       OutputString(" for toolchain ");
       OutputString(
           target->label().GetToolchainLabel().GetUserVisibleName(false),
@@ -234,9 +237,9 @@
 
   auto ResolveSuggestion = [&](std::string_view value) {
     const auto& [targets, ok] = ResolveSuggestionToTarget(
-        &setup->build_settings(), all_targets, current_toolchain, value);
+        build_settings, all_targets, current_toolchain, value);
     if (!ok) {
-      OutputError();
+      StartError();
       if (value.starts_with("//")) {
         OutputString("Could not find target or file ");
         OutputQuoted(value);
@@ -255,12 +258,12 @@
     return false;
 
   if (includer_targets.empty()) {
-    OutputError();
+    StartError();
     OutputQuoted(includer_name);
     OutputString(" did not resolve to any targets\n");
     return false;
   } else if (includer_targets.size() > 1) {
-    OutputError();
+    StartError();
     OutputQuoted(includer_name);
     OutputString(" resolved to multiple targets\n");
     for (const auto& [target, is_private] : includer_targets) {
@@ -287,7 +290,8 @@
   if (targets.empty()) {
     OutputQuoted(included_name);
     OutputString(" is not in the headers of any targets.\n");
-    OutputSuggestion("Add ");
+    StartSuggestion();
+    OutputString("Add ");
     OutputQuoted(included_name);
     OutputString(" to a target's public headers");
     return true;
@@ -316,7 +320,7 @@
   }
 
   if (targets.size() > 1) {
-    OutputWarning();
+    StartWarning();
     OutputQuoted(included_name);
     OutputString(" is ambiguous because it belongs to multiple targets:\n");
     for (const auto& [target, _] : targets) {
@@ -324,7 +328,8 @@
       OutputTarget(target);
       OutputString("\n");
     }
-    OutputSuggestion(
+    StartSuggestion();
+    OutputString(
         "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);
@@ -333,11 +338,12 @@
 
   const auto& [included, included_dep_kind] = targets.front();
   if (included_dep_kind == commands::ApiScope::kPrivate) {
-    OutputWarning();
+    StartWarning();
     OutputQuoted(included_name);
     OutputString(" is in the private API of ");
     OutputTarget(included);
-    OutputSuggestion("Move ");
+    StartSuggestion();
+    OutputString("Move ");
     OutputQuoted(included_name);
     OutputString(" from `sources` to `public` in ");
     OutputDefinition(included);
@@ -366,6 +372,19 @@
 }
 
 int RunSuggest(const std::vector<std::string>& args) {
+  constexpr auto kLabelLike = TextDecoration::DECORATION_GREEN;
+
+  auto OutputError = [](std::string_view message) {
+    OutputString("Error: ", TextDecoration::DECORATION_RED);
+    OutputString(message);
+  };
+
+  auto OutputQuoted = [](std::string_view message) {
+    OutputString("\"", kLabelLike);
+    OutputString(message, kLabelLike);
+    OutputString("\"", kLabelLike);
+  };
+
   if (args.size() <= 1) {
     OutputError("gn suggest requires arguments. See \"gn help suggest\"\n");
     return 1;
@@ -404,7 +423,12 @@
       OutputString(":\n");
     }
 
-    success &= OutputSuggestions(all_targets, setup, includer, included);
+    success &= OutputSuggestions(
+        all_targets, &setup->build_settings(),
+        setup->loader()->default_toolchain_label(), includer, included,
+        [](std::string_view str, TextDecoration dec, HtmlEscaping esc) {
+          ::OutputString(str, dec, esc);
+        });
   }
 
   return success ? 0 : 1;
diff --git a/src/gn/commands.h b/src/gn/commands.h
index fcc5bd8..62d0ffb 100644
--- a/src/gn/commands.h
+++ b/src/gn/commands.h
@@ -5,6 +5,7 @@
 #ifndef TOOLS_GN_COMMANDS_H_
 #define TOOLS_GN_COMMANDS_H_
 
+#include <functional>
 #include <map>
 #include <set>
 #include <string>
@@ -12,6 +13,7 @@
 #include <vector>
 
 #include "base/values.h"
+#include "gn/standard_out.h"
 #include "gn/target.h"
 #include "gn/unique_vector.h"
 
@@ -103,6 +105,15 @@
 extern const char kSuggest_Help[];
 int RunSuggest(const std::vector<std::string>& args);
 
+using OutputStringFunc =
+    std::function<void(std::string_view, TextDecoration, HtmlEscaping)>;
+bool OutputSuggestions(const std::vector<const Target*>& all_targets,
+                       const BuildSettings* build_settings,
+                       const Label& default_toolchain,
+                       std::string_view includer_name,
+                       std::string_view included_name,
+                       OutputStringFunc output_fn);
+
 extern const char kCleanStale[];
 extern const char kCleanStale_HelpShort[];
 extern const char kCleanStale_Help[];