Accept multiple label patterns in gn check "gn check" now takes any number of label patterns and checks the targets matching any of them. As before, label patterns bypass check_targets, no_check_targets and no_check_generated_targets in .gn. Target selection moves to GetTargetsToCheck() so that it can be unit tested. RunCheck() still calls Builder::GetAllResolvedTargets() once. Split out of https://gn-review.googlesource.com/c/gn/+/27840. Bug: 577807023 Change-Id: Icd11cbf7fbdc7ffac41520ea3893f1d8da4f81c2 Reviewed-on: https://gn-review.googlesource.com/c/gn/+/28520 Commit-Queue: Ryosuke Miyake <rysk@google.com> Reviewed-by: Matt Stark <msta@google.com> Reviewed-by: Takuto Ikuta <tikuta@google.com>
diff --git a/build/gen.py b/build/gen.py index 2730b9e..237ae22 100755 --- a/build/gen.py +++ b/build/gen.py
@@ -917,6 +917,7 @@ 'src/gn/bundle_data_unittest.cc', 'src/gn/c_include_iterator_unittest.cc', 'src/gn/check_includes_strict_exceptions_unittest.cc', + 'src/gn/command_check_unittest.cc', 'src/gn/command_format_unittest.cc', 'src/gn/command_suggest_unittest.cc', 'src/gn/commands_unittest.cc',
diff --git a/docs/reference.md b/docs/reference.md index c9754c3..925db25 100644 --- a/docs/reference.md +++ b/docs/reference.md
@@ -367,7 +367,7 @@ given arguments set (which may affect the values of other arguments). ``` -### <a name="cmd_check"></a>**gn check <out_dir> [<label_pattern>] [\--force] [\--check-generated] [\--fix]** [Back to Top](#gn-reference) +### <a name="cmd_check"></a>**gn check <out_dir> [<label_pattern>...] [\--force] [\--check-generated] [\--fix]** [Back to Top](#gn-reference) ``` GN's include header checker validates that the includes for C-like source @@ -377,9 +377,10 @@ this command does not write out any build files. It's intended to be an easy way to manually trigger include file checking. - The <label_pattern> can take exact labels or patterns that match more than - one (although not general regular expressions). If specified, only those - matching targets will be checked. See "gn help label_pattern" for details. + Each <label_pattern> can be an exact label or a pattern that matches more + than one (although not general regular expressions). If specified, only + those matching targets will be checked. See "gn help label_pattern" for + details. ``` #### **Command-specific switches**
diff --git a/src/gn/command_check.cc b/src/gn/command_check.cc index 8c032d2..13eaf09 100644 --- a/src/gn/command_check.cc +++ b/src/gn/command_check.cc
@@ -4,7 +4,10 @@ #include <stddef.h> +#include <optional> #include <tuple> +#include <utility> +#include <vector> #include "base/command_line.h" #include "base/strings/stringprintf.h" @@ -57,7 +60,7 @@ const char kCheck[] = "check"; const char kCheck_HelpShort[] = "check: Check header dependencies."; const char kCheck_Help[] = - R"(gn check <out_dir> [<label_pattern>] [--force] [--check-generated] [--fix] + R"(gn check <out_dir> [<label_pattern>...] [--force] [--check-generated] [--fix] GN's include header checker validates that the includes for C-like source files match the build dependency graph. @@ -66,9 +69,10 @@ this command does not write out any build files. It's intended to be an easy way to manually trigger include file checking. - The <label_pattern> can take exact labels or patterns that match more than - one (although not general regular expressions). If specified, only those - matching targets will be checked. See "gn help label_pattern" for details. + Each <label_pattern> can be an exact label or a pattern that matches more + than one (although not general regular expressions). If specified, only + those matching targets will be checked. See "gn help label_pattern" for + details. Command-specific switches @@ -187,10 +191,71 @@ Check only the files in targets in the //foo directory tree. )"; +namespace { + +// Returns the targets matching label_patterns. Returns std::nullopt if the +// patterns are invalid or match no target. +std::optional<std::vector<const Target*>> ResolveCandidates( + Setup* setup, + const std::vector<std::string>& label_patterns, + bool default_toolchain_only) { + UniqueVector<const Target*> target_matches; + UniqueVector<const Config*> config_matches; + UniqueVector<const Toolchain*> toolchain_matches; + UniqueVector<SourceFile> file_matches; + if (!ResolveFromCommandLineInput( + setup, label_patterns, default_toolchain_only, &target_matches, + &config_matches, &toolchain_matches, &file_matches)) + return std::nullopt; + + if (target_matches.size() == 0) { + OutputString("No matching targets.\n"); + return std::nullopt; + } + return target_matches.release(); +} + +} // namespace + +std::optional<TargetsToCheck> GetTargetsToCheck( + Setup* setup, + const std::vector<const Target*>& all_targets, + const std::vector<std::string>& label_patterns, + bool default_toolchain_only) { + TargetsToCheck result; + if (label_patterns.empty()) { + result.candidates = all_targets; + } else if (auto resolved = ResolveCandidates(setup, label_patterns, + default_toolchain_only)) { + result.candidates = std::move(*resolved); + } else { + return std::nullopt; + } + + if (!label_patterns.empty()) { + // An explicit check of targets bypasses the lists in the .gn file. + result.to_check = result.candidates; + return result; + } + + if (setup->check_patterns()) { + FilterTargetsByPatterns(result.candidates, *setup->check_patterns(), + &result.to_check); + } else if (setup->no_check_patterns()) { + FilterOutTargetsByPatterns(result.candidates, *setup->no_check_patterns(), + &result.to_check); + } else { + result.to_check = result.candidates; + } + FilterTargetsByPatterns(result.to_check, setup->no_check_generated_patterns(), + &result.no_check_generated); + return result; +} + int RunCheck(const std::vector<std::string>& args) { - if (args.size() != 1 && args.size() != 2) { + if (args.empty()) { Err(Location(), "Unknown command format. See \"gn help check\"", - "Usage: \"gn check <out_dir> [<target_label>]\"") + "Usage: \"gn check <out_dir> [<label_pattern>...]\"") .PrintToStdout(); return 1; } @@ -204,82 +269,43 @@ const base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess(); bool default_toolchain_only = cmdline->HasSwitch(switches::kDefaultToolchain); - - std::vector<const Target*> all_targets = - setup->builder().GetAllResolvedTargets(); - - bool filtered_by_build_config = false; - std::vector<const Target*> targets_to_check; - if (args.size() > 1) { - // Compute the targets to check. - std::vector<std::string> inputs(args.begin() + 1, args.end()); - UniqueVector<const Target*> target_matches; - UniqueVector<const Config*> config_matches; - UniqueVector<const Toolchain*> toolchain_matches; - UniqueVector<SourceFile> file_matches; - if (!ResolveFromCommandLineInput(setup, inputs, default_toolchain_only, - &target_matches, &config_matches, - &toolchain_matches, &file_matches)) - return 1; - - if (target_matches.size() == 0) { - OutputString("No matching targets.\n"); - return 1; - } - targets_to_check.insert(targets_to_check.begin(), target_matches.begin(), - target_matches.end()); - } else { - // No argument means to check everything allowed by the filter in - // the build config file. - if (setup->check_patterns()) { - FilterTargetsByPatterns(all_targets, *setup->check_patterns(), - &targets_to_check); - filtered_by_build_config = targets_to_check.size() != all_targets.size(); - } else if (setup->no_check_patterns()) { - FilterOutTargetsByPatterns(all_targets, *setup->no_check_patterns(), - &targets_to_check); - filtered_by_build_config = targets_to_check.size() != all_targets.size(); - } else { - // No global filter, check everything. - targets_to_check = all_targets; - } - } - bool force = cmdline->HasSwitch("force"); bool check_generated = cmdline->HasSwitch("check-generated"); bool check_system = setup->check_system_includes() || cmdline->HasSwitch("check-system"); bool fix = cmdline->HasSwitch("fix"); - // Like check_targets and no_check_targets, no_check_generated_targets is not - // used when a label pattern is given. It only matters when generated files - // are checked. - std::vector<const Target*> no_check_generated; - if (args.size() == 1 && check_generated) { - FilterTargetsByPatterns(targets_to_check, - setup->no_check_generated_patterns(), - &no_check_generated); - } + std::vector<std::string> label_patterns(args.begin() + 1, args.end()); + std::vector<const Target*> all_targets = + setup->builder().GetAllResolvedTargets(); + std::optional<TargetsToCheck> targets = GetTargetsToCheck( + setup, all_targets, label_patterns, default_toolchain_only); + if (!targets) + return 1; + + // no_check_generated_targets only matters when generated files are checked. + if (!check_generated) + targets->no_check_generated.clear(); if (!CheckPublicHeaders(&setup->build_settings(), all_targets, - targets_to_check, no_check_generated, force, + targets->to_check, targets->no_check_generated, force, check_generated, check_system, fix, setup)) return 1; if (!base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kQuiet)) { - if (filtered_by_build_config) { + if (targets->to_check.size() != targets->candidates.size()) { // Tell the user about the implicit filtering since this is obscure. OutputString(base::StringPrintf( "%d targets out of %d checked based on the check_targets or " "no_check_targets defined in \".gn\".\n", - static_cast<int>(targets_to_check.size()), - static_cast<int>(all_targets.size()))); + static_cast<int>(targets->to_check.size()), + static_cast<int>(targets->candidates.size()))); } - if (!no_check_generated.empty()) { + if (!targets->no_check_generated.empty()) { OutputString(base::StringPrintf( "Generated files of %d targets not checked based on the " "no_check_generated_targets defined in \".gn\".\n", - static_cast<int>(no_check_generated.size()))); + static_cast<int>(targets->no_check_generated.size()))); } OutputString("Header dependency check OK\n", DECORATION_GREEN); }
diff --git a/src/gn/command_check_unittest.cc b/src/gn/command_check_unittest.cc new file mode 100644 index 0000000..33c0247 --- /dev/null +++ b/src/gn/command_check_unittest.cc
@@ -0,0 +1,174 @@ +// 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 <map> +#include <optional> +#include <set> +#include <string> +#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/filesystem_utils.h" +#include "gn/setup.h" +#include "gn/source_file.h" +#include "gn/switches.h" +#include "gn/target.h" +#include "gn/test_with_scheduler.h" +#include "util/test/test.h" + +namespace { + +using CommandCheckTest = TestWithScheduler; + +// //:a depends on //:b, which depends on //:d. //:a also has a data dep on +// //:c. //:unrelated is not reachable from //:a. +constexpr char kDotFile[] = R"( +buildconfig = "//BUILDCONFIG.gn" +no_check_targets = [ "//:d" ] +no_check_generated_targets = [ "//:b" ] +)"; + +constexpr char kBuildConfig[] = R"( +set_default_toolchain("//toolchain:default") +)"; + +constexpr char kToolchain[] = R"( +toolchain("default") { + tool("cxx") { + command = "cxx" + outputs = [ "{{source_out_dir}}/{{source_file_part}}.o" ] + } + tool("link") { + command = "link" + outputs = [ "{{root_out_dir}}/{{target_output_name}}{{output_extension}}" ] + } + tool("stamp") { + command = "stamp" + } +} +)"; + +constexpr char kBuildFile[] = R"( +executable("a") { + sources = [ "a.cc" ] + deps = [ ":b" ] + data_deps = [ ":c" ] +} +source_set("b") { + sources = [ "b.h" ] + deps = [ ":d" ] +} +source_set("c") { + sources = [ "c.h" ] +} +source_set("d") { + sources = [ "d.h" ] +} +source_set("unrelated") { + sources = [ "unrelated.h" ] +} +)"; + +class TestProject { + public: + explicit TestProject(const char* dot_file = kDotFile) { + EXPECT_TRUE(in_temp_dir_.CreateUniqueTempDir()); + EXPECT_TRUE(build_temp_dir_.CreateUniqueTempDir()); + base::FilePath in_path = base::MakeAbsoluteFilePath(in_temp_dir_.GetPath()); + base::FilePath build_path = + base::MakeAbsoluteFilePath(build_temp_dir_.GetPath()); + + const std::map<std::string, std::string> files = { + {".gn", dot_file}, + {"BUILDCONFIG.gn", kBuildConfig}, + {"toolchain/BUILD.gn", kToolchain}, + {"BUILD.gn", kBuildFile}, + }; + for (const auto& [name, content] : files) { + base::FilePath full_path = in_path.AppendASCII(name); + base::CreateDirectory(full_path.DirName()); + WriteFile(full_path, content, nullptr); + } + + base::CommandLine cmdline(base::CommandLine::NO_PROGRAM); + cmdline.AppendSwitchPath(switches::kRoot, in_path); + EXPECT_TRUE(setup_.DoSetup(FilePathToUTF8(build_path), true, cmdline)); + EXPECT_TRUE(setup_.Run()); + } + + Setup* setup() { return &setup_; } + std::vector<const Target*> all_targets() { + return setup_.builder().GetAllResolvedTargets(); + } + + private: + base::ScopedTempDir in_temp_dir_; + base::ScopedTempDir build_temp_dir_; + Setup setup_; +}; + +std::set<std::string> Labels(const std::vector<const Target*>& targets) { + std::set<std::string> labels; + for (const Target* target : targets) + labels.insert(target->label().GetUserVisibleName(false)); + return labels; +} + +} // namespace + +TEST_F(CommandCheckTest, NoLabelPatterns) { + TestProject project; + std::optional<commands::TargetsToCheck> targets = + commands::GetTargetsToCheck(project.setup(), project.all_targets(), {}, + /*default_toolchain_only=*/false); + ASSERT_TRUE(targets); + + EXPECT_EQ( + (std::set<std::string>{"//:a", "//:b", "//:c", "//:d", "//:unrelated"}), + Labels(targets->candidates)); + EXPECT_EQ((std::set<std::string>{"//:a", "//:b", "//:c", "//:unrelated"}), + Labels(targets->to_check)); + EXPECT_EQ((std::set<std::string>{"//:b"}), + Labels(targets->no_check_generated)); +} + +TEST_F(CommandCheckTest, LabelPatternsBypassDotFileLists) { + TestProject project; + std::optional<commands::TargetsToCheck> targets = commands::GetTargetsToCheck( + project.setup(), project.all_targets(), {"//:b", "//:d"}, + /*default_toolchain_only=*/false); + ASSERT_TRUE(targets); + + EXPECT_EQ((std::set<std::string>{"//:b", "//:d"}), + Labels(targets->candidates)); + EXPECT_EQ((std::set<std::string>{"//:b", "//:d"}), Labels(targets->to_check)); + EXPECT_TRUE(targets->no_check_generated.empty()); +} + +TEST_F(CommandCheckTest, NoLabelPatternsCheckTargets) { + TestProject project(R"( +buildconfig = "//BUILDCONFIG.gn" +check_targets = [ "//:a", "//:c" ] +)"); + std::optional<commands::TargetsToCheck> targets = + commands::GetTargetsToCheck(project.setup(), project.all_targets(), {}, + /*default_toolchain_only=*/false); + ASSERT_TRUE(targets); + + EXPECT_EQ( + (std::set<std::string>{"//:a", "//:b", "//:c", "//:d", "//:unrelated"}), + Labels(targets->candidates)); + EXPECT_EQ((std::set<std::string>{"//:a", "//:c"}), Labels(targets->to_check)); + EXPECT_TRUE(targets->no_check_generated.empty()); +} + +TEST_F(CommandCheckTest, NoMatchingTargets) { + TestProject project; + EXPECT_FALSE(commands::GetTargetsToCheck( + project.setup(), project.all_targets(), {"//nonexistent/*"}, + /*default_toolchain_only=*/false)); +}
diff --git a/src/gn/commands.h b/src/gn/commands.h index efa9775..60de9e4 100644 --- a/src/gn/commands.h +++ b/src/gn/commands.h
@@ -416,6 +416,32 @@ UniqueVector<const Toolchain*>* toolchain_matches, UniqueVector<SourceFile>* file_matches); +// The targets that "gn check" checks. See GetTargetsToCheck(). +struct TargetsToCheck { + // All targets if no label pattern is given, or else the targets matching the + // label patterns. + std::vector<const Target*> candidates; + + // The candidates allowed by check_targets or no_check_targets in the .gn + // file. + std::vector<const Target*> to_check; + + // The targets in |to_check| that match no_check_generated_targets. + std::vector<const Target*> no_check_generated; +}; + +// Computes the targets that "gn check" checks. |all_targets| must be all +// resolved targets in the build; they are the candidates if |label_patterns| +// is empty. Label patterns bypass the .gn lists: |to_check| gets all candidates +// and |no_check_generated| stays empty. +// +// On failure, prints the error and returns std::nullopt. +std::optional<TargetsToCheck> GetTargetsToCheck( + Setup* setup, + const std::vector<const Target*>& all_targets, + const std::vector<std::string>& label_patterns, + bool default_toolchain_only); + // Runs the header checker. All targets in the build should be given in // all_targets, and the specific targets to check should be in to_check. //