Add --exclude-type command-line argument

It essentially does the opposite of `--type`. It's needed for efficient
target filtering in `autotest.py` in Chromium:
https://chromium-review.googlesource.com/c/chromium/src/+/8139431

Bug: 534608615
Change-Id: If5b2e4ab8cad5529fbc3477adaac83f86a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/24520
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: Zgroza (Luke) Klimek <zgroza@chromium.org>
Reviewed-by: Andrew Grieve <agrieve@google.com>
diff --git a/docs/reference.md b/docs/reference.md
index 62eb514..b726a19 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -670,7 +670,7 @@
       tree.
 
       Tree output can not be used with the filtering or output flags: --as,
-      --type, --testonly.
+      --type, --exclude-type, --testonly.
 
   --type=(action|bundle_data|copy|create_bundle|executable|
           generated_file|group|loadable_module|rust_library|
@@ -678,6 +678,16 @@
       Restrict outputs to targets matching the given type. If
       unspecified, no filtering will be performed. You can specify
       a comma-separated list of types to match multiple types.
+      Can not be used with --exclude-type.
+
+  --exclude-type=(action|bundle_data|copy|create_bundle|executable|
+                  generated_file|group|loadable_module|rust_library|
+                  rust_proc_macro|shared_library|source_set|
+                  static_library)
+      Exclude targets matching the given type from the outputs. If
+      unspecified, no filtering will be performed. You can specify
+      a comma-separated list of types to match multiple types.
+      Can not be used with --type.
 ```
 
 #### **Note**
@@ -1047,7 +1057,7 @@
 ```
 ### <a name="cmd_ls"></a>**gn ls &lt;out_dir&gt; [&lt;label_pattern&gt;] [\--default-toolchain] [\--as=...]**&nbsp;[Back to Top](#gn-reference)
 ```
-      [--type=...] [--testonly=...]
+      [--type=...|--exclude-type=...] [--testonly=...]
 
   Lists all targets matching the given pattern for the given build directory.
   By default, only targets in the default toolchain will be matched unless a
@@ -1092,6 +1102,16 @@
       Restrict outputs to targets matching the given type. If
       unspecified, no filtering will be performed. You can specify
       a comma-separated list of types to match multiple types.
+      Can not be used with --exclude-type.
+
+  --exclude-type=(action|bundle_data|copy|create_bundle|executable|
+                  generated_file|group|loadable_module|rust_library|
+                  rust_proc_macro|shared_library|source_set|
+                  static_library)
+      Exclude targets matching the given type from the outputs. If
+      unspecified, no filtering will be performed. You can specify
+      a comma-separated list of types to match multiple types.
+      Can not be used with --type.
 ```
 
 #### **Examples**
@@ -1276,7 +1296,8 @@
 
 ```
   gn refs <out_dir> (<label_pattern>|<label>|<file>|@<response_file>)* [--all]
-          [--default-toolchain] [--as=...] [--testonly=...] [--type=...]
+          [--default-toolchain] [--as=...] [--testonly=...]
+          [--type=...|--exclude-type=...]
 
   Finds reverse dependencies (which targets reference something). The input is
   a list containing:
@@ -1348,7 +1369,7 @@
       be elided. Combine with --all to see a full dependency tree.
 
       Tree output can not be used with the filtering or output flags: --as,
-      --type, --testonly.
+      --type, --exclude-type, --testonly.
 
   --type=(action|bundle_data|copy|create_bundle|executable|
           generated_file|group|loadable_module|rust_library|
@@ -1356,6 +1377,16 @@
       Restrict outputs to targets matching the given type. If
       unspecified, no filtering will be performed. You can specify
       a comma-separated list of types to match multiple types.
+      Can not be used with --exclude-type.
+
+  --exclude-type=(action|bundle_data|copy|create_bundle|executable|
+                  generated_file|group|loadable_module|rust_library|
+                  rust_proc_macro|shared_library|source_set|
+                  static_library)
+      Exclude targets matching the given type from the outputs. If
+      unspecified, no filtering will be performed. You can specify
+      a comma-separated list of types to match multiple types.
+      Can not be used with --type.
 
   --relation=(source|public|input|data|script|output)
       Restricts output to targets which refer to input files by a specific
diff --git a/src/gn/command_desc.cc b/src/gn/command_desc.cc
index e7b0c99..5b9dd2b 100644
--- a/src/gn/command_desc.cc
+++ b/src/gn/command_desc.cc
@@ -604,7 +604,7 @@
       tree.
 
       Tree output can not be used with the filtering or output flags: --as,
-      --type, --testonly.
+      --type, --exclude-type, --testonly.
 
 )"
 
diff --git a/src/gn/command_ls.cc b/src/gn/command_ls.cc
index 0dbf911..b2879ac 100644
--- a/src/gn/command_ls.cc
+++ b/src/gn/command_ls.cc
@@ -19,7 +19,7 @@
 const char kLs_HelpShort[] = "ls: List matching targets.";
 const char kLs_Help[] =
     R"(gn ls <out_dir> [<label_pattern>] [--default-toolchain] [--as=...]
-      [--type=...] [--testonly=...]
+      [--type=...|--exclude-type=...] [--testonly=...]
 
   Lists all targets matching the given pattern for the given build directory.
   By default, only targets in the default toolchain will be matched unless a
diff --git a/src/gn/command_refs.cc b/src/gn/command_refs.cc
index 0e7770b..4f08886 100644
--- a/src/gn/command_refs.cc
+++ b/src/gn/command_refs.cc
@@ -237,7 +237,8 @@
     R"(gn refs
 
   gn refs <out_dir> (<label_pattern>|<label>|<file>|@<response_file>)* [--all]
-          [--default-toolchain] [--as=...] [--testonly=...] [--type=...]
+          [--default-toolchain] [--as=...] [--testonly=...]
+          [--type=...|--exclude-type=...]
 
   Finds reverse dependencies (which targets reference something). The input is
   a list containing:
@@ -292,7 +293,7 @@
       be elided. Combine with --all to see a full dependency tree.
 
       Tree output can not be used with the filtering or output flags: --as,
-      --type, --testonly.
+      --type, --exclude-type, --testonly.
 
 )"
 
diff --git a/src/gn/commands.cc b/src/gn/commands.cc
index a2cca9a..582ba01 100644
--- a/src/gn/commands.cc
+++ b/src/gn/commands.cc
@@ -4,6 +4,7 @@
 
 #include "gn/commands.h"
 
+#include <algorithm>
 #include <fstream>
 #include <optional>
 
@@ -153,6 +154,18 @@
   return true;
 }
 
+// Returns the target exclude type filter based on the command line flags for
+// the current process. Returns true on success. On error, prints a message to
+// the console and returns false.
+//
+// The vector will be empty if there is no filter. Target::ACTION_FOREACH
+// will never be returned. Code applying the filters should apply Target::ACTION
+// to both ACTION and ACTION_FOREACH.
+bool GetTargetExcludeTypeFilter(std::vector<Target::OutputType>* types) {
+  *types = CommandSwitches::Get().target_exclude_types();
+  return true;
+}
+
 // Applies any testonly filtering specified on the command line to the given
 // target set. On failure, prints an error and returns false.
 bool ApplyTestonlyFilter(std::vector<const Target*>* targets) {
@@ -177,13 +190,16 @@
   return true;
 }
 
-// Applies any target type filtering specified on the command line to the given
-// target set. On failure, prints an error and returns false.
+// Applies any target type and exclude type filtering specified on the command
+// line to the given target set. On failure, prints an error and returns false.
 bool ApplyTypeFilter(std::vector<const Target*>* targets) {
   std::vector<Target::OutputType> types;
   if (!GetTargetTypeFilter(&types))
     return false;
-  if (targets->empty() || types.empty())
+  std::vector<Target::OutputType> exclude_types;
+  if (!GetTargetExcludeTypeFilter(&exclude_types))
+    return false;
+  if (targets->empty() || (types.empty() && exclude_types.empty()))
     return true;  // Nothing to filter out.
 
   // Filter into a copy of the vector, then replace the output.
@@ -191,14 +207,18 @@
   result.reserve(targets->size());
 
   for (const Target* target : *targets) {
-    if (std::ranges::any_of(types, [target](Target::OutputType type) {
-          // Make "action" also apply to ACTION_FOREACH.
-          return target->output_type() == type ||
-                 (type == Target::ACTION &&
-                  target->output_type() == Target::ACTION_FOREACH);
-        })) {
-      result.push_back(target);
+    Target::OutputType target_type =
+        target->output_type() == Target::ACTION_FOREACH ? Target::ACTION
+                                                        : target->output_type();
+    if (!types.empty() &&
+        std::ranges::find(types, target_type) == types.end()) {
+      continue;
     }
+    if (!exclude_types.empty() &&
+        std::ranges::find(exclude_types, target_type) != exclude_types.end()) {
+      continue;
+    }
+    result.push_back(target);
   }
 
   *targets = std::move(result);
@@ -464,8 +484,19 @@
   }
 
   std::string_view target_type_switch = "type";
-  if (cmdline.HasSwitch(target_type_switch)) {
-    std::string value = cmdline.GetSwitchValueString(target_type_switch);
+  std::string_view target_exclude_type_switch = "exclude-type";
+  if (cmdline.HasSwitch(target_type_switch) &&
+      cmdline.HasSwitch(target_exclude_type_switch)) {
+    Err(Location(), "Can't specify both \"--type\" and \"--exclude-type\".")
+        .PrintToStdout();
+    return false;
+  }
+  if (cmdline.HasSwitch(target_type_switch) ||
+      cmdline.HasSwitch(target_exclude_type_switch)) {
+    std::string_view switch_name = cmdline.HasSwitch(target_type_switch)
+                                       ? target_type_switch
+                                       : target_exclude_type_switch;
+    std::string value = cmdline.GetSwitchValueString(switch_name);
     std::vector<std::string> tokens = base::SplitString(
         value, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
     static const struct {
@@ -486,17 +517,21 @@
         {"bundle_data", Target::BUNDLE_DATA},
         {"create_bundle", Target::CREATE_BUNDLE},
     };
+    std::vector<Target::OutputType>& target_types_vector =
+        cmdline.HasSwitch(target_type_switch) ? result.target_types_
+                                              : result.target_exclude_types_;
     for (const std::string& token : tokens) {
       bool found = false;
       for (const auto& type : kTypes) {
         if (token == type.name) {
-          result.target_types_.push_back(type.type);
+          target_types_vector.push_back(type.type);
           found = true;
           break;
         }
       }
       if (!found) {
-        Err(Location(), "Invalid value \"" + token + "\" for \"--type\".")
+        Err(Location(), "Invalid value \"" + token + "\" for \"--" +
+                            std::string(switch_name) + "\".")
             .PrintToStdout();
         return false;
       }
diff --git a/src/gn/commands.h b/src/gn/commands.h
index 27fd81e..812d342 100644
--- a/src/gn/commands.h
+++ b/src/gn/commands.h
@@ -202,6 +202,11 @@
     return target_types_;
   }
 
+  // For --exclude-type=TARGET_TYPE
+  const std::vector<Target::OutputType>& target_exclude_types() const {
+    return target_exclude_types_;
+  }
+
   enum TestonlyMode {
     TESTONLY_NONE,   // no --testonly used.
     TESTONLY_FALSE,  // --testonly=false
@@ -248,6 +253,7 @@
 
   TargetPrintMode target_print_mode_ = TARGET_PRINT_LABEL;
   std::vector<Target::OutputType> target_types_;
+  std::vector<Target::OutputType> target_exclude_types_;
   TestonlyMode testonly_mode_ = TESTONLY_NONE;
 
   std::string meta_rebase_dir_;
@@ -362,21 +368,32 @@
   "      output\n"                                                            \
   "          Prints the first output file for the target relative to the\n"   \
   "          root build directory.\n"
-#define TARGET_TYPE_FILTER_COMMAND_LINE_HELP                              \
-  "  --type=(action|bundle_data|copy|create_bundle|executable|\n"         \
-  "          generated_file|group|loadable_module|rust_library|\n"        \
-  "          rust_proc_macro|shared_library|source_set|static_library)\n" \
-  "      Restrict outputs to targets matching the given type. If\n"       \
-  "      unspecified, no filtering will be performed. You can specify\n"  \
-  "      a comma-separated list of types to match multiple types.\n"
+#define TARGET_TYPE_FILTER_COMMAND_LINE_HELP                               \
+  "  --type=(action|bundle_data|copy|create_bundle|executable|\n"          \
+  "          generated_file|group|loadable_module|rust_library|\n"         \
+  "          rust_proc_macro|shared_library|source_set|static_library)\n"  \
+  "      Restrict outputs to targets matching the given type. If\n"        \
+  "      unspecified, no filtering will be performed. You can specify\n"   \
+  "      a comma-separated list of types to match multiple types.\n"       \
+  "      Can not be used with --exclude-type.\n"                           \
+  "\n"                                                                     \
+  "  --exclude-type=(action|bundle_data|copy|create_bundle|executable|\n"  \
+  "                  generated_file|group|loadable_module|rust_library|\n" \
+  "                  rust_proc_macro|shared_library|source_set|\n"         \
+  "                  static_library)\n"                                    \
+  "      Exclude targets matching the given type from the outputs. If\n"   \
+  "      unspecified, no filtering will be performed. You can specify\n"   \
+  "      a comma-separated list of types to match multiple types.\n"       \
+  "      Can not be used with --type.\n"
 #define TARGET_TESTONLY_FILTER_COMMAND_LINE_HELP                           \
   "  --testonly=(true|false)\n"                                            \
   "      Restrict outputs to targets with the testonly flag set\n"         \
   "      accordingly. When unspecified, the target's testonly flags are\n" \
   "      ignored.\n"
 
-// Applies any testonly and type filters specified on the command line,
-// and prints the targets as specified by the --as command line flag.
+// Applies any testonly, type, and exclude-type filters specified on the
+// command line, and prints the targets as specified by the --as command line
+// flag.
 //
 // If indent is true, the results will be indented two spaces.
 //
diff --git a/src/gn/commands_unittest.cc b/src/gn/commands_unittest.cc
index eb95f68..8c7ce5f 100644
--- a/src/gn/commands_unittest.cc
+++ b/src/gn/commands_unittest.cc
@@ -109,4 +109,125 @@
     commands::CommandSwitches empty_switches;
     commands::CommandSwitches::Set(empty_switches);
   }
+
+  for (const auto& test_case : cases) {
+    std::vector<const Target*> targets_to_filter = all_targets;
+
+    base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);
+    cmdline.AppendSwitch("exclude-type", test_case.first);
+
+    commands::CommandSwitches::Init(cmdline);
+
+    base::ListValue out;
+    commands::FilterAndPrintTargets(&targets_to_filter, &out);
+
+    ASSERT_EQ(all_targets.size() - 1u, targets_to_filter.size())
+        << "Failed for type: " << test_case.first;
+    EXPECT_FALSE(std::ranges::any_of(targets_to_filter,
+                                     [&test_case](const Target* target) {
+                                       return target->output_type() ==
+                                              test_case.second;
+                                     }))
+        << "Failed for type: " << test_case.first;
+
+    commands::CommandSwitches empty_switches;
+    commands::CommandSwitches::Set(empty_switches);
+  }
+
+  // Test multiple exclude types separated by a comma.
+  {
+    std::vector<const Target*> targets_to_filter = all_targets;
+
+    base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);
+    cmdline.AppendSwitch("exclude-type", "executable,rust_library");
+
+    commands::CommandSwitches::Init(cmdline);
+
+    base::ListValue out;
+    commands::FilterAndPrintTargets(&targets_to_filter, &out);
+
+    ASSERT_EQ(all_targets.size() - 2u, targets_to_filter.size());
+    EXPECT_FALSE(
+        std::ranges::any_of(targets_to_filter, [](const Target* target) {
+          return target->output_type() == Target::EXECUTABLE;
+        }));
+    EXPECT_FALSE(
+        std::ranges::any_of(targets_to_filter, [](const Target* target) {
+          return target->output_type() == Target::RUST_LIBRARY;
+        }));
+
+    commands::CommandSwitches empty_switches;
+    commands::CommandSwitches::Set(empty_switches);
+  }
+
+  // Test that supplying both --type and --exclude-type fails.
+  {
+    base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);
+    cmdline.AppendSwitch("type", "executable");
+    cmdline.AppendSwitch("exclude-type", "shared_library");
+
+    EXPECT_FALSE(commands::CommandSwitches::Init(cmdline));
+  }
+
+  // Test invalid switch values for --type and --exclude-type.
+  {
+    base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);
+    cmdline.AppendSwitch("type", "invalid_type");
+    EXPECT_FALSE(commands::CommandSwitches::Init(cmdline));
+  }
+  {
+    base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);
+    cmdline.AppendSwitch("exclude-type", "invalid_type");
+    EXPECT_FALSE(commands::CommandSwitches::Init(cmdline));
+  }
+
+  // Test Target::ACTION_FOREACH aliasing with --exclude-type=action.
+  {
+    auto action_foreach_target = std::make_unique<Target>(
+        setup.settings(), Label(SourceDir("//a/"), "action_foreach_target"));
+    action_foreach_target->set_output_type(Target::ACTION_FOREACH);
+    std::vector<const Target*> targets_to_filter = all_targets;
+    targets_to_filter.push_back(action_foreach_target.get());
+
+    base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);
+    cmdline.AppendSwitch("exclude-type", "action");
+
+    commands::CommandSwitches::Init(cmdline);
+
+    base::ListValue out;
+    commands::FilterAndPrintTargets(&targets_to_filter, &out);
+
+    EXPECT_FALSE(
+        std::ranges::any_of(targets_to_filter, [](const Target* target) {
+          return target->output_type() == Target::ACTION ||
+                 target->output_type() == Target::ACTION_FOREACH;
+        }));
+
+    commands::CommandSwitches empty_switches;
+    commands::CommandSwitches::Set(empty_switches);
+  }
+
+  // Test combined --exclude-type and --testonly filtering.
+  {
+    std::vector<const Target*> targets_to_filter = all_targets;
+    // Mark one target as testonly=true.
+    created_targets[0]->set_testonly(true);
+
+    base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);
+    cmdline.AppendSwitch("exclude-type", "shared_library");
+    cmdline.AppendSwitch("testonly", "true");
+
+    commands::CommandSwitches::Init(cmdline);
+
+    base::ListValue out;
+    commands::FilterAndPrintTargets(&targets_to_filter, &out);
+
+    for (const Target* target : targets_to_filter) {
+      EXPECT_TRUE(target->testonly());
+      EXPECT_NE(target->output_type(), Target::SHARED_LIBRARY);
+    }
+
+    commands::CommandSwitches empty_switches;
+    commands::CommandSwitches::Set(empty_switches);
+  }
 }