Add check_includes_strict_exceptions configuration to .gn
Allows configuring strict deps include check exemptions via a list of
scopes in .gn:
check_includes_strict_exceptions = [
{
includer = "//some/dir/*"
included = "//some/target" # optional, omit for global
},
]
This provides fine-grained, target-level exemptions for check_includes_strict
without requiring expensive regex filtering or custom parser formats.
Bug: 556959580
Change-Id: I2306f74835db8299e9e924637ac4bd686a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/27340
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: Matt Stark <msta@google.com>
diff --git a/build/gen.py b/build/gen.py
index 5c49973..57f148e 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -804,6 +804,7 @@
'src/gn/c_include_iterator.cc',
'src/gn/c_substitution_type.cc',
'src/gn/c_tool.cc',
+ 'src/gn/check_includes_strict_exceptions.cc',
'src/gn/command_analyze.cc',
'src/gn/command_args.cc',
'src/gn/command_check.cc',
@@ -980,6 +981,7 @@
'src/gn/binary_target_generator_unittest.cc',
'src/gn/bundle_data_unittest.cc',
'src/gn/c_include_iterator_unittest.cc',
+ 'src/gn/check_includes_strict_exceptions_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 3201930..3b55a39 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -7731,6 +7731,15 @@
default. They can be checked explicitly by running
"gn check --check-system" or "gn gen --check=system"
+ check_includes_strict_exceptions [optional]
+ A list of scopes containing exceptions for strict dependencies checking.
+ Each scope in this list specifies an exception rule:
+ includer = <from_pattern>
+ included = <to_label> [optional]
+ which waives check_includes_strict violations from targets matching
+ <from_pattern> trying to include headers from <to_label>
+ (or to headers from any target if included is omitted).
+
exec_script_allowlist [optional]
A list of .gn/.gni files (not labels) that have permission to call the
exec_script function. If this list is defined, calls to exec_script will
diff --git a/src/gn/build_settings.h b/src/gn/build_settings.h
index 8330c7d..7feef27 100644
--- a/src/gn/build_settings.h
+++ b/src/gn/build_settings.h
@@ -13,6 +13,7 @@
#include "base/files/file_path.h"
#include "gn/args.h"
+#include "gn/check_includes_strict_exceptions.h"
#include "gn/label.h"
#include "gn/label_pattern.h"
#include "gn/scope.h"
@@ -165,6 +166,15 @@
allow_circular_includes_from_allowlist_ = std::move(list);
}
+ const CheckIncludesStrictExceptions& check_includes_strict_exceptions()
+ const {
+ return check_includes_strict_exceptions_;
+ }
+ void set_check_includes_strict_exceptions(
+ CheckIncludesStrictExceptions exceptions) {
+ check_includes_strict_exceptions_ = std::move(exceptions);
+ }
+
// If true, all generated_file() metadata walks include metadata from
// validations targets.
bool experimental_collect_validations_metadata() const {
@@ -191,6 +201,7 @@
SourceFile build_config_file_;
SourceFile arg_file_template_path_;
+ CheckIncludesStrictExceptions check_includes_strict_exceptions_;
SourceDir build_dir_;
Args build_args_;
diff --git a/src/gn/check_includes_strict_exceptions.cc b/src/gn/check_includes_strict_exceptions.cc
new file mode 100644
index 0000000..a680838
--- /dev/null
+++ b/src/gn/check_includes_strict_exceptions.cc
@@ -0,0 +1,73 @@
+// 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 "gn/check_includes_strict_exceptions.h"
+
+#include <string_view>
+
+#include "gn/scope.h"
+#include "gn/value.h"
+
+bool CheckIncludesStrictExceptions::CanInclude(const Label& from,
+ const Label& to) const {
+ if (LabelPattern::VectorMatches(global_, from))
+ return true;
+
+ auto it = specific_targets_.find(to.GetWithNoToolchain());
+ if (it != specific_targets_.end())
+ return LabelPattern::VectorMatches(it->second, from);
+
+ return false;
+}
+
+Result<CheckIncludesStrictExceptions> CheckIncludesStrictExceptions::FromValue(
+ const Value& value,
+ const SourceDir& current_dir,
+ std::string_view source_root) {
+ RETURN_IF_ERROR(value.VerifyTypeIs(Value::LIST));
+
+ CheckIncludesStrictExceptions exceptions;
+ for (const auto& item : value.list_value()) {
+ RETURN_IF_ERROR(item.VerifyTypeIs(Value::SCOPE));
+
+ const Scope* scope = item.scope_value();
+ const Value* includer = scope->GetValue("includer");
+ if (!includer)
+ return Err(item, "Exception rule requires \"includer\".");
+
+ for (auto [key, val] : scope->GetCurrentScopeValues()) {
+ if (key != "includer" && key != "included") {
+ return Err(
+ *val,
+ "Unknown key in exception rule: \"" + std::string(key) + "\".",
+ "Only \"includer\" and \"included\" are allowed.");
+ }
+ }
+
+ Err err;
+ LabelPattern from =
+ LabelPattern::GetPattern(current_dir, source_root, *includer, &err);
+ RETURN_IF_ERROR(err);
+
+ if (!includer->string_value().starts_with("//"))
+ return Err(*includer,
+ "Label pattern must be absolute (starting with '//').");
+
+ const Value* included = scope->GetValue("included");
+ if (included) {
+ Label to =
+ Label::Resolve(current_dir, source_root, Label(), *included, &err);
+ RETURN_IF_ERROR(err);
+
+ if (!included->string_value().starts_with("//"))
+ return Err(*included,
+ "Target label must be absolute (starting with '//').");
+ exceptions.AddException(from, to);
+ } else {
+ exceptions.AddGlobalException(from);
+ }
+ }
+
+ return exceptions;
+}
diff --git a/src/gn/check_includes_strict_exceptions.h b/src/gn/check_includes_strict_exceptions.h
new file mode 100644
index 0000000..c1e4c53
--- /dev/null
+++ b/src/gn/check_includes_strict_exceptions.h
@@ -0,0 +1,43 @@
+// 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.
+
+#ifndef TOOLS_GN_CHECK_INCLUDES_STRICT_EXCEPTIONS_H_
+#define TOOLS_GN_CHECK_INCLUDES_STRICT_EXCEPTIONS_H_
+
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+#include "gn/err.h"
+#include "gn/label.h"
+#include "gn/label_pattern.h"
+#include "gn/source_dir.h"
+
+class Value;
+
+// CheckIncludesStrictExceptions stores allowlist rules for
+// check_includes_strict. An includer LabelPattern can be allowed to include
+// from any target (global) or from a specific target Label.
+class CheckIncludesStrictExceptions {
+ public:
+ static Result<CheckIncludesStrictExceptions> FromValue(
+ const Value& value,
+ const SourceDir& current_dir,
+ std::string_view source_root);
+
+ void AddGlobalException(const LabelPattern& from) { global_.push_back(from); }
+ void AddException(const LabelPattern& from, const Label& to) {
+ specific_targets_[to.GetWithNoToolchain()].push_back(from);
+ }
+
+ // Checks whether `from` is allowed to include headers from `to`.
+ bool CanInclude(const Label& from, const Label& to) const;
+
+ private:
+ std::vector<LabelPattern> global_;
+ std::unordered_map<Label, std::vector<LabelPattern>> specific_targets_;
+};
+
+#endif // TOOLS_GN_CHECK_INCLUDES_STRICT_EXCEPTIONS_H_
diff --git a/src/gn/check_includes_strict_exceptions_unittest.cc b/src/gn/check_includes_strict_exceptions_unittest.cc
new file mode 100644
index 0000000..7794eb7
--- /dev/null
+++ b/src/gn/check_includes_strict_exceptions_unittest.cc
@@ -0,0 +1,92 @@
+// 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 "gn/check_includes_strict_exceptions.h"
+
+#include <memory>
+#include <string>
+#include <string_view>
+
+#include "gn/label.h"
+#include "gn/scope.h"
+#include "gn/source_dir.h"
+#include "gn/value.h"
+#include "util/test/test.h"
+
+TEST(CheckIncludesStrictExceptionsTest, FromValue) {
+ auto parse = [](const Value& val) {
+ return CheckIncludesStrictExceptions::FromValue(val, SourceDir("//"), "");
+ };
+
+ auto make_rule = [](std::string_view from, std::string_view to) {
+ auto scope = std::make_unique<Scope>(static_cast<const Settings*>(nullptr));
+ scope->SetValue("includer", Value(nullptr, std::string(from)), nullptr);
+ if (!to.empty())
+ scope->SetValue("included", Value(nullptr, std::string(to)), nullptr);
+ Value v(nullptr, Value::SCOPE);
+ v.SetScopeValue(std::move(scope));
+ return v;
+ };
+
+ EXPECT_FAILURE(parse(Value(nullptr, "not_a_list")));
+
+ Value bad_list(nullptr, Value::LIST);
+ bad_list.list_value().push_back(Value(nullptr, "not_a_scope"));
+ EXPECT_FAILURE(parse(bad_list));
+
+ Value non_abs(nullptr, Value::LIST);
+ non_abs.list_value().push_back(make_rule("not_abs:a", "//b:b"));
+ EXPECT_FAILURE(parse(non_abs));
+
+ // Specific target exception.
+ Value specific_list(nullptr, Value::LIST);
+ specific_list.list_value().push_back(make_rule("//:includer", "//:included"));
+ auto specific_exceptions = parse(specific_list);
+ ASSERT_SUCCESS(specific_exceptions);
+ EXPECT_TRUE(specific_exceptions->CanInclude(
+ Label(SourceDir("//"), "includer"), Label(SourceDir("//"), "included")));
+ EXPECT_FALSE(specific_exceptions->CanInclude(
+ Label(SourceDir("//"), "includer"), Label(SourceDir("//"), "other")));
+
+ // Global exception (included omitted).
+ Value global_list(nullptr, Value::LIST);
+ global_list.list_value().push_back(make_rule("//:includer", ""));
+ auto global_exceptions = parse(global_list);
+ ASSERT_SUCCESS(global_exceptions);
+ EXPECT_TRUE(
+ global_exceptions->CanInclude(Label(SourceDir("//"), "includer"),
+ Label(SourceDir("//any/"), "target")));
+ EXPECT_FALSE(
+ global_exceptions->CanInclude(Label(SourceDir("//other/"), "target"),
+ Label(SourceDir("//any/"), "target")));
+}
+
+TEST(CheckIncludesStrictExceptionsTest, CanInclude) {
+ CheckIncludesStrictExceptions exceptions;
+
+ exceptions.AddException(
+ LabelPattern(LabelPattern::RECURSIVE_DIRECTORY,
+ SourceDir("//includer_recursive/"), "", Label()),
+ Label(SourceDir("//included/"), "target"));
+
+ EXPECT_TRUE(exceptions.CanInclude(
+ Label(SourceDir("//includer_recursive/sub/"), "target"),
+ Label(SourceDir("//included/"), "target")));
+ EXPECT_FALSE(
+ exceptions.CanInclude(Label(SourceDir("//other_dir/"), "target"),
+ Label(SourceDir("//included/"), "target")));
+ EXPECT_FALSE(exceptions.CanInclude(
+ Label(SourceDir("//includer_recursive/sub/"), "target"),
+ Label(SourceDir("//included/"), "other_target")));
+
+ exceptions.AddGlobalException(LabelPattern(
+ LabelPattern::DIRECTORY, SourceDir("//includer_global/"), "", Label()));
+
+ EXPECT_TRUE(
+ exceptions.CanInclude(Label(SourceDir("//includer_global/"), "foo"),
+ Label(SourceDir("//any_dir/"), "any_target")));
+ EXPECT_FALSE(
+ exceptions.CanInclude(Label(SourceDir("//other_dir/"), "foo"),
+ Label(SourceDir("//any_dir/"), "any_target")));
+}
diff --git a/src/gn/command_suggest.cc b/src/gn/command_suggest.cc
index 9f9c7a9..5442eb3 100644
--- a/src/gn/command_suggest.cc
+++ b/src/gn/command_suggest.cc
@@ -824,7 +824,9 @@
Label label = target->label();
HeaderChecker::Chain cycle;
if (cache.GetReachabilityCache(target).SearchForDependencyTo(
- includer, /*permitted=*/false, &cycle)) {
+ includer,
+ HeaderChecker::ReachabilityCache::DependencyType::kNonPublic,
+ &cycle)) {
std::reverse(cycle.begin(), cycle.end());
StartWarning();
OutputTarget(target);
diff --git a/src/gn/header_checker.cc b/src/gn/header_checker.cc
index 97ea8b9..7f31064 100644
--- a/src/gn/header_checker.cc
+++ b/src/gn/header_checker.cc
@@ -16,6 +16,7 @@
#include "gn/build_settings.h"
#include "gn/builder.h"
#include "gn/c_include_iterator.h"
+#include "gn/check_includes_strict_exceptions.h"
#include "gn/config.h"
#include "gn/config_values_extractors.h"
#include "gn/err.h"
@@ -83,7 +84,8 @@
// Given a reverse dependency chain where the target chain[0]'s includes are
// being used by chain[end] and not all deps are public, returns the string
// describing the error.
-std::string GetDependencyChainPublicError(const HeaderChecker::Chain& chain) {
+std::string GetDependencyChainPublicError(const HeaderChecker::Chain& chain,
+ bool strict_exempt) {
std::string ret =
"The target:\n " +
chain[chain.size() - 1].target->label().GetUserVisibleName(false) +
@@ -99,10 +101,13 @@
} else {
// Indirect dependency chain, print the chain.
const Target* strict_target = nullptr;
- for (size_t i = 1; i < chain.size() - 1; i++) {
- if (chain[i].target->check_includes_strict()) {
- strict_target = chain[i].target;
- break;
+ if (!strict_exempt) {
+ for (size_t i = 1; i < chain.size() - 1; i++) {
+ if (chain[i - 1].is_public &&
+ chain[i].target->check_includes_strict()) {
+ strict_target = chain[i].target;
+ break;
+ }
}
}
@@ -249,8 +254,10 @@
DoWorkChunked(
pool, targets_to_precompute, 32,
[this](std::span<const Target* const> chunk) {
- for (const Target* target : chunk)
- GetReachabilityCacheForTarget(target).PerformDependencyWalk(true);
+ for (const Target* target : chunk) {
+ GetReachabilityCacheForTarget(target).PerformDependencyWalk(
+ ReachabilityCache::DependencyType::kPermitted);
+ }
});
}
@@ -427,16 +434,14 @@
return SourceFile();
}
-void HeaderChecker::ReachabilityCache::PerformDependencyWalk(bool permitted) {
+void HeaderChecker::ReachabilityCache::PerformDependencyWalk(
+ DependencyType type) {
// Conduct the actual BFS.
- if (permitted ? permitted_complete_.load(std::memory_order_relaxed)
- : any_complete_.load(std::memory_order_relaxed)) {
+ auto& walk = walks_[static_cast<size_t>(type)];
+ if (walk.complete.load(std::memory_order_relaxed)) {
return;
}
- BreadcrumbTable& breadcrumbs =
- permitted ? permitted_breadcrumbs_ : any_breadcrumbs_;
-
// work_queue maintains a queue of targets which need to be considered as part
// of dependency chain, in the order they were first traversed. Each time a
// new transitive dependency of source_target_ is discovered for the first
@@ -444,67 +449,62 @@
// which target it was reached from when first discovered.
base::queue<const Target*> work_queue;
work_queue.push(source_target_);
- breadcrumbs.Insert(source_target_, nullptr, true);
+ walk.breadcrumbs.Insert(source_target_, nullptr, true);
while (!work_queue.empty()) {
const Target* target = work_queue.front();
work_queue.pop();
- if (permitted && target != source_target_ &&
+ if (type == DependencyType::kPermitted && target != source_target_ &&
target->check_includes_strict()) {
continue;
}
for (const auto& dep : target->public_deps()) {
- if (breadcrumbs.Insert(dep.ptr, target, true))
+ if (walk.breadcrumbs.Insert(dep.ptr, target, true))
work_queue.push(dep.ptr);
}
- if (!permitted || target == source_target_) {
+ if (type == DependencyType::kNonPublic || target == source_target_) {
// Consider all dependencies since all target paths are allowed, so add
// in private ones. Also do this the first time through the loop, since
// a target can include headers from its direct deps regardless of
// public/private-ness.
for (const auto& dep : target->private_deps()) {
- if (breadcrumbs.Insert(dep.ptr, target, false))
+ if (walk.breadcrumbs.Insert(dep.ptr, target, false))
work_queue.push(dep.ptr);
}
}
}
- if (permitted) {
- permitted_complete_.store(true, std::memory_order_release);
- } else {
- any_complete_.store(true, std::memory_order_release);
- }
+
+ walk.complete.store(true, std::memory_order_release);
}
bool HeaderChecker::ReachabilityCache::SearchForDependencyTo(
const Target* search_for,
- bool permitted,
+ DependencyType type,
Chain* chain) {
- if (permitted ? permitted_complete_.load(std::memory_order_acquire)
- : any_complete_.load(std::memory_order_acquire)) {
- return SearchBreadcrumbs(search_for, permitted, chain);
+ auto& walk = walks_[static_cast<size_t>(type)];
+ if (walk.complete.load(std::memory_order_acquire)) {
+ return SearchBreadcrumbs(search_for, type, chain);
}
{
std::unique_lock<std::shared_mutex> write_lock(lock_);
- if (!(permitted ? permitted_complete_.load(std::memory_order_relaxed)
- : any_complete_.load(std::memory_order_relaxed))) {
- PerformDependencyWalk(permitted);
+ if (!walk.complete.load(std::memory_order_relaxed)) {
+ PerformDependencyWalk(type);
}
}
- return SearchBreadcrumbs(search_for, permitted, chain);
+ return SearchBreadcrumbs(search_for, type, chain);
}
bool HeaderChecker::ReachabilityCache::SearchBreadcrumbs(
const Target* search_for,
- bool permitted,
+ DependencyType type,
Chain* chain) const {
- const BreadcrumbTable& breadcrumbs =
- permitted ? permitted_breadcrumbs_ : any_breadcrumbs_;
- ChainLink incoming_link = breadcrumbs.GetLink(search_for);
+ const auto& walk = walks_[static_cast<size_t>(type)];
+ ChainLink incoming_link = walk.breadcrumbs.GetLink(search_for);
if (!incoming_link.target)
return false;
@@ -512,7 +512,7 @@
chain->clear();
const Target* cur = search_for;
while (cur != source_target_) {
- ChainLink link = breadcrumbs.GetLink(cur);
+ ChainLink link = walk.breadcrumbs.GetLink(cur);
chain->push_back(ChainLink(cur, link.is_public));
cur = link.target;
}
@@ -676,6 +676,18 @@
// includes to detect when A implicitly includes C's header. This will not
// have the annoying false positive problem, but is complex to write.
+ // Some files are declared in multiple targets.
+ // While it does make exemptions a little overly broad, it does become much
+ // simpler this way, and it's pretty rare anyway.
+ bool strict_exempt = false;
+ for (const auto& target : targets) {
+ if (build_settings_->check_includes_strict_exceptions().CanInclude(
+ from_target->label(), target.target->label())) {
+ strict_exempt = true;
+ break;
+ }
+ }
+
// Fast path: the include is valid if any target containing this header is
// the includer itself, allowlists the includer, or is reachable through a
// permitted dependency chain.
@@ -683,7 +695,7 @@
const Target* to_target = target.target;
if (to_target == from_target) {
if (from_target->check_includes_strict() && is_public_header &&
- !target.is_public) {
+ !target.is_public && !strict_exempt) {
break;
}
return;
@@ -697,12 +709,17 @@
if (!target.is_public && !FriendMatches(to_target, from_target))
continue;
- if (!from_target_cache.SearchForDependencyTo(to_target, true, &chain))
+ auto search_type =
+ strict_exempt
+ ? ReachabilityCache::DependencyType::kPermittedIgnoringStrict
+ : ReachabilityCache::DependencyType::kPermitted;
+ if (!from_target_cache.SearchForDependencyTo(to_target, search_type,
+ &chain))
continue;
DCHECK(chain.size() >= 2);
if (from_target->check_includes_strict() && is_public_header &&
- !chain[chain.size() - 2].is_public) {
+ !chain[chain.size() - 2].is_public && !strict_exempt) {
continue;
}
return;
@@ -759,7 +776,7 @@
} else {
last_error = Err(CreatePersistentRange(source_file, range),
"Can't include this header from here.",
- GetDependencyChainPublicError(chain));
+ GetDependencyChainPublicError(chain, strict_exempt));
}
}
@@ -800,13 +817,15 @@
}
// 1. Try permitted dependency.
- if (from_target_cache.SearchForDependencyTo(search_for, true, chain)) {
+ if (from_target_cache.SearchForDependencyTo(
+ search_for, ReachabilityCache::DependencyType::kPermitted, chain)) {
*is_permitted = true;
return true;
}
// 2. Try any dependency.
- if (from_target_cache.SearchForDependencyTo(search_for, false, chain)) {
+ if (from_target_cache.SearchForDependencyTo(
+ search_for, ReachabilityCache::DependencyType::kNonPublic, chain)) {
*is_permitted = false;
return true;
}
diff --git a/src/gn/header_checker.h b/src/gn/header_checker.h
index 14f4eaf..9009128 100644
--- a/src/gn/header_checker.h
+++ b/src/gn/header_checker.h
@@ -72,9 +72,6 @@
// Store the shortest-dependency-path information for all BFS walks starting
// from a given `search_from` target.
//
- // `permitted_breadcrumbs` corresponds to public dependencies only.
- // `any_breadcrumbs` corresponds to all dependencies.
- //
// Each walk type needs only to be performed once, which is recorded by the
// corresponding completion flag.
class ReachabilityCache {
@@ -83,6 +80,17 @@
ReachabilityCache(const ReachabilityCache&) = delete;
ReachabilityCache& operator=(const ReachabilityCache&) = delete;
+ enum class DependencyType : size_t {
+ // Searches across public_deps, stopping at targets for which
+ // check_includes_strict is true.
+ kPermitted,
+ // Searches across public deps.
+ kPermittedIgnoringStrict,
+ // Searches across deps as well as public_deps.
+ kNonPublic,
+ };
+ static constexpr size_t kNumDependencyTypes = 3;
+
const Target* source_target() const { return source_target_; }
// Returns true if the given `search_for` target is reachable from
@@ -90,16 +98,13 @@
//
// If found, the vector given in `chain` will be filled with the reverse
// dependency chain from the destination target to the source target.
- //
- // If `permitted` is true, only permitted (public) dependency paths are
- // searched.
bool SearchForDependencyTo(const Target* search_for,
- bool permitted,
+ DependencyType type,
Chain* chain);
// Conducts a breadth-first search through the dependency graph to find a
// shortest chain from source_target_.
- void PerformDependencyWalk(bool permitted);
+ void PerformDependencyWalk(DependencyType type);
private:
// Header checking structures ----------------------------------------------
@@ -189,19 +194,19 @@
// Reconstructs the shortest dependency chain to the given target if it was
// found during a previous walk of the given type. Returns true on success.
bool SearchBreadcrumbs(const Target* search_for,
- bool permitted,
+ DependencyType type,
Chain* chain) const;
const Target* source_target_;
mutable std::shared_mutex lock_;
- // Breadcrumbs for the shortest permitted path.
- BreadcrumbTable permitted_breadcrumbs_;
- // Breadcrumbs for the shortest path of any type.
- BreadcrumbTable any_breadcrumbs_;
- std::atomic<bool> permitted_complete_ = false;
- std::atomic<bool> any_complete_ = false;
+ struct WalkData {
+ BreadcrumbTable breadcrumbs;
+ std::atomic<bool> complete = false;
+ };
+ // Indexed by DependencyType.
+ std::array<WalkData, kNumDependencyTypes> walks_;
};
// check_generated, if true, will also check generated
@@ -246,6 +251,7 @@
CheckIncludesStrictPrivateInPublicHeader);
FRIEND_TEST_ALL_PREFIXES(HeaderCheckerTest,
CheckIncludesStrictSameTargetPrivateHeader);
+ FRIEND_TEST_ALL_PREFIXES(HeaderCheckerTest, CheckIncludesStrictExceptions);
FRIEND_TEST_ALL_PREFIXES(HeaderCheckerTest, NoCheckGenerated);
~HeaderChecker();
diff --git a/src/gn/header_checker_unittest.cc b/src/gn/header_checker_unittest.cc
index e575ec3..2d41462 100644
--- a/src/gn/header_checker_unittest.cc
+++ b/src/gn/header_checker_unittest.cc
@@ -9,6 +9,9 @@
#include <unordered_set>
#include <vector>
+#include "gn/check_includes_strict_exceptions.h"
+#include "gn/input_file.h"
+#include "gn/label_pattern.h"
#include "gn/scheduler.h"
#include "gn/substitution_list.h"
#include "gn/target.h"
@@ -651,3 +654,87 @@
// Listing another target does not affect A.
EXPECT_EQ(files_to_check(true, {&b_}), (Files{handwritten, generated}));
}
+
+TEST_F(HeaderCheckerTest, CheckIncludesStrictExceptions) {
+ InputFile input_file(SourceFile("//a_public.h"));
+ input_file.SetContents(std::string());
+ LocationRange range;
+
+ // Setup: a_ is strict, has private dep on b_, and has private header.
+ a_.public_deps().clear();
+ a_.private_deps().push_back(LabelTargetPair(&b_));
+ a_.private_deps().push_back(LabelTargetPair(&d_));
+
+ SourceFile a_public("//a_public.h");
+ SourceFile a_private("//a_private.h");
+ a_.sources().push_back(a_public);
+ a_.sources().push_back(a_private);
+ a_.public_headers().push_back(a_public);
+ a_.set_all_headers_public(false);
+ a_.set_check_includes_strict(true);
+
+ SourceFile b_public("//b_public.h");
+ b_.sources().push_back(b_public);
+
+ SourceFile c_public("//c_public.h");
+ c_.sources().push_back(c_public);
+ b_.set_check_includes_strict(true);
+
+ SourceFile d_public("//d_public.h");
+ SourceFile d_private("//d_private.h");
+ d_.sources().push_back(d_public);
+ d_.sources().push_back(d_private);
+ d_.public_headers().push_back(d_public);
+ d_.set_all_headers_public(false);
+ d_.set_check_includes_strict(true);
+
+ Err err;
+ d_.OnResolved(&err);
+ ASSERT_SUCCESS(err);
+ b_.OnResolved(&err);
+ ASSERT_SUCCESS(err);
+ a_.OnResolved(&err);
+ ASSERT_SUCCESS(err);
+
+ auto checker = CreateChecker();
+ auto& a_cache = checker->GetReachabilityCacheForTarget(&a_);
+ auto& d_cache = checker->GetReachabilityCacheForTarget(&d_);
+
+ auto check_ok = [&](const scoped_refptr<HeaderChecker>& c,
+ HeaderChecker::ReachabilityCache& cache,
+ bool is_public_header, const SourceFile& file) {
+ std::vector<Err> errors;
+ c->CheckInclude(cache, is_public_header, input_file, file, range, &errors);
+ return errors.empty();
+ };
+
+ // Without exceptions, includes fail:
+ EXPECT_FALSE(check_ok(checker, a_cache, true, b_public));
+ EXPECT_FALSE(check_ok(checker, a_cache, true, a_private));
+ EXPECT_FALSE(check_ok(checker, a_cache, false, c_public));
+ EXPECT_FALSE(check_ok(checker, a_cache, true, d_public));
+ EXPECT_FALSE(check_ok(checker, d_cache, true, d_private));
+
+ // Set exceptions on build_settings:
+ CheckIncludesStrictExceptions exceptions;
+ LabelPattern a_dir(LabelPattern::DIRECTORY, SourceDir("//a/"), "", Label());
+ exceptions.AddException(a_dir, b_.label());
+ exceptions.AddException(a_dir, a_.label());
+ exceptions.AddException(a_dir, c_.label());
+ setup_.build_settings()->set_check_includes_strict_exceptions(
+ std::move(exceptions));
+
+ // Exempt includes with exceptions:
+ EXPECT_TRUE(check_ok(checker, a_cache, true, b_public));
+ EXPECT_TRUE(check_ok(checker, a_cache, true, a_private));
+ EXPECT_TRUE(check_ok(checker, a_cache, false, c_public));
+
+ // Non-exempt includes with exceptions:
+ // Inter-target: a_ includes d_public, but //a:* => //d:* is not in
+ // exceptions.
+ EXPECT_FALSE(check_ok(checker, a_cache, true, d_public));
+
+ // Intra-target: d_ includes d_private, but //d:* => //d:* is not in
+ // exceptions.
+ EXPECT_FALSE(check_ok(checker, d_cache, true, d_private));
+}
diff --git a/src/gn/setup.cc b/src/gn/setup.cc
index c91fbf1..6abf641 100644
--- a/src/gn/setup.cc
+++ b/src/gn/setup.cc
@@ -19,6 +19,7 @@
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
+#include "gn/check_includes_strict_exceptions.h"
#include "gn/command_format.h"
#include "gn/commands.h"
#include "gn/exec_process.h"
@@ -137,6 +138,15 @@
default. They can be checked explicitly by running
"gn check --check-system" or "gn gen --check=system"
+ check_includes_strict_exceptions [optional]
+ A list of scopes containing exceptions for strict dependencies checking.
+ Each scope in this list specifies an exception rule:
+ includer = <from_pattern>
+ included = <to_label> [optional]
+ which waives check_includes_strict violations from targets matching
+ <from_pattern> trying to include headers from <to_label>
+ (or to headers from any target if included is omitted).
+
exec_script_allowlist [optional]
A list of .gn/.gni files (not labels) that have permission to call the
exec_script function. If this list is defined, calls to exec_script will
@@ -1240,6 +1250,20 @@
check_system_includes_ = check_system_includes_value->boolean_value();
}
+ const Value* check_includes_strict_exceptions_value =
+ dotfile_scope_.GetValue("check_includes_strict_exceptions", true);
+ if (check_includes_strict_exceptions_value) {
+ auto result = CheckIncludesStrictExceptions::FromValue(
+ *check_includes_strict_exceptions_value, current_dir,
+ build_settings_.root_path_utf8());
+ if (!result.has_value()) {
+ *err = result.error();
+ return false;
+ }
+ build_settings_.set_check_includes_strict_exceptions(
+ std::move(result.value()));
+ }
+
// Fill exec_script_allowlist.
const Value* exec_script_allowlist_value =
dotfile_scope_.GetValue("exec_script_allowlist", true);