#include "gn/header_checker.h"
#include <algorithm>
#include "base/containers/queue.h"
#include "base/files/file_util.h"
#include "base/strings/string_util.h"
#include "gn/build_settings.h"
#include "gn/builder.h"
#include "gn/c_include_iterator.h"
#include "gn/config.h"
#include "gn/config_values_extractors.h"
#include "gn/err.h"
#include "gn/filesystem_utils.h"
#include "gn/hash_table_base.h"
#include "gn/scheduler.h"
#include "gn/swift_values.h"
#include "gn/target.h"
#include "gn/trace.h"
#include "util/worker_pool.h"
namespace {
struct PublicGeneratedPair {
PublicGeneratedPair() : is_public(false), is_generated(false) {}
bool is_public;
bool is_generated;
};
LocationRange CreatePersistentRange(const InputFile& input_file,
const LocationRange& range) {
InputFile* clone_input_file;
std::vector<Token>* tokens;
std::unique_ptr<ParseNode>* parse_root;
g_scheduler->input_file_manager()->AddDynamicInput(
input_file.name(), &clone_input_file, &tokens, &parse_root);
clone_input_file->SetContents(input_file.contents());
return LocationRange(Location(clone_input_file, range.begin().line_number(),
range.begin().column_number()),
Location(clone_input_file, range.end().line_number(),
range.end().column_number()));
}
std::string GetDependencyChainPublicError(const HeaderChecker::Chain& chain) {
std::string ret =
"The target:\n " +
chain[chain.size() - 1].target->label().GetUserVisibleName(false) +
"\nis including a file from the target:\n " +
chain[0].target->label().GetUserVisibleName(false) + "\n";
DCHECK(chain.size() != 1 && chain.size() != 2);
if (chain.empty()) {
ret += "There is no dependency chain between these targets.";
} else {
ret +=
"\nIt's usually best to depend directly on the destination target.\n"
"In some cases, the destination target is considered a subcomponent\n"
"of an intermediate target. In this case, the intermediate target\n"
"should depend publicly on the destination to forward the ability\n"
"to include headers.\n"
"\n"
"Dependency chain (there may also be others):\n";
for (int i = static_cast<int>(chain.size()) - 1; i >= 0; i--) {
ret.append(" " + chain[i].target->label().GetUserVisibleName(false));
if (i != 0) {
if (i == static_cast<int>(chain.size()) - 1 || chain[i - 1].is_public)
ret.append(" -->");
else
ret.append(" --[private]-->");
}
ret.append("\n");
}
}
return ret;
}
bool TargetLabelsMatchExceptToolchain(const Target* a, const Target* b) {
return a->label().dir() == b->label().dir() &&
a->label().name() == b->label().name();
}
bool FriendMatches(const Target* annotation_on,
const Target* is_marked_friend) {
return LabelPattern::VectorMatches(annotation_on->friends(),
is_marked_friend->label());
}
}
HeaderChecker::HeaderChecker(const BuildSettings* build_settings,
const std::vector<const Target*>& targets,
bool check_generated,
bool check_system)
: build_settings_(build_settings),
check_generated_(check_generated),
check_system_(check_system),
errors_lock_(),
task_count_cv_() {
for (auto* target : targets)
AddTargetToFileMap(target, &file_map_);
}
HeaderChecker::~HeaderChecker() = default;
bool HeaderChecker::Run(const std::vector<const Target*>& to_check,
bool force_check,
std::vector<Err>* errors) {
FileMap files_to_check;
for (auto* check : to_check) {
if (check->IsBinary())
AddTargetToFileMap(check, &files_to_check);
}
RunCheckOverFiles(files_to_check, force_check);
if (errors_.empty())
return true;
*errors = errors_;
return false;
}
void HeaderChecker::RunCheckOverFiles(const FileMap& files, bool force_check) {
WorkerPool pool;
task_count_.Increment();
for (const auto& file : files) {
const SourceFile::Type type = file.first.GetType();
if (type != SourceFile::SOURCE_CPP && type != SourceFile::SOURCE_H &&
type != SourceFile::SOURCE_C && type != SourceFile::SOURCE_M &&
type != SourceFile::SOURCE_MM && type != SourceFile::SOURCE_RC)
continue;
if (!check_generated_) {
bool is_generated = false;
for (const auto& vect_i : file_map_[file.first])
is_generated |= vect_i.is_generated;
if (is_generated)
continue;
}
std::vector<const Target*> targets_to_check;
for (const auto& vect_i : file.second) {
if (vect_i.target->check_includes()) {
targets_to_check.push_back(vect_i.target);
}
}
if (targets_to_check.empty())
continue;
task_count_.Increment();
pool.PostTask([this, targets = std::move(targets_to_check),
file = file.first]() { DoWork(targets, file); });
}
task_count_.Decrement();
std::unique_lock<std::mutex> auto_lock(task_count_lock_);
while (!task_count_.IsZero())
task_count_cv_.wait(auto_lock);
}
void HeaderChecker::DoWork(const std::vector<const Target*>& targets,
const SourceFile& file) {
std::vector<Err> errors;
if (!CheckFile(targets, file, &errors)) {
std::lock_guard<std::mutex> lock(errors_lock_);
errors_.insert(errors_.end(), errors.begin(), errors.end());
}
if (!task_count_.Decrement()) {
std::unique_lock<std::mutex> auto_lock(task_count_lock_);
task_count_cv_.notify_one();
}
}
void HeaderChecker::AddTargetToFileMap(const Target* target, FileMap* dest) {
bool default_public = target->all_headers_public();
std::map<SourceFile, PublicGeneratedPair> files_to_public;
for (const auto& source : target->sources()) {
files_to_public[source].is_public = default_public;
}
if (default_public)
DCHECK(target->public_headers().empty());
for (const auto& source : target->public_headers()) {
files_to_public[source].is_public = true;
}
if (target->builds_swift_module()) {
const SourceFile& bridge_header = target->swift_values().bridge_header();
if (!bridge_header.is_null()) {
files_to_public[bridge_header].is_public = default_public;
}
std::vector<SourceFile> outputs;
target->swift_values().GetOutputsAsSourceFiles(target, &outputs);
for (const SourceFile& output : outputs) {
if (output.GetType() == SourceFile::SOURCE_H) {
PublicGeneratedPair* pair = &files_to_public[output];
pair->is_public = true;
pair->is_generated = true;
}
}
}
std::vector<SourceFile> outputs;
target->action_values().GetOutputsAsSourceFiles(target, &outputs);
for (const auto& output : outputs) {
PublicGeneratedPair* pair = &files_to_public[output];
pair->is_public = true;
pair->is_generated = true;
}
for (const auto& cur : files_to_public) {
(*dest)[cur.first].push_back(
TargetInfo(target, cur.second.is_public, cur.second.is_generated));
}
}
bool HeaderChecker::IsFileInOuputDir(const SourceFile& file) const {
const std::string& build_dir = build_settings_->build_dir().value();
return file.value().compare(0, build_dir.size(), build_dir) == 0;
}
SourceFile HeaderChecker::SourceFileForInclude(
const IncludeStringWithLocation& include,
const std::vector<SourceDir>& include_dirs,
const InputFile& source_file,
Err* err) const {
using base::FilePath;
Value relative_file_value(nullptr, std::string(include.contents));
auto find_predicate = [relative_file_value, err,
this](const SourceDir& dir) -> bool {
SourceFile include_file = dir.ResolveRelativeFile(relative_file_value, err);
return file_map_.find(include_file) != file_map_.end();
};
if (!include.system_style_include) {
const SourceDir& file_dir = source_file.dir();
if (find_predicate(file_dir)) {
return file_dir.ResolveRelativeFile(relative_file_value, err);
}
}
auto it =
std::find_if(include_dirs.begin(), include_dirs.end(), find_predicate);
if (it != include_dirs.end())
return it->ResolveRelativeFile(relative_file_value, err);
return SourceFile();
}
void HeaderChecker::ReachabilityCache::PerformDependencyWalk(bool permitted) {
BreadcrumbTable& breadcrumbs =
permitted ? permitted_breadcrumbs_ : any_breadcrumbs_;
bool& complete = permitted ? permitted_complete_ : any_complete_;
if (complete)
return;
base::queue<const Target*> work_queue;
work_queue.push(source_target_);
breadcrumbs.Insert(source_target_, nullptr, true);
while (!work_queue.empty()) {
const Target* target = work_queue.front();
work_queue.pop();
for (const auto& dep : target->public_deps()) {
if (breadcrumbs.Insert(dep.ptr, target, true))
work_queue.push(dep.ptr);
}
if (!permitted || target == source_target_) {
for (const auto& dep : target->private_deps()) {
if (breadcrumbs.Insert(dep.ptr, target, false))
work_queue.push(dep.ptr);
}
}
}
complete = true;
}
bool HeaderChecker::ReachabilityCache::SearchForDependencyTo(
const Target* search_for,
bool permitted,
Chain* chain) {
{
std::shared_lock<std::shared_mutex> read_lock(lock_);
if (permitted ? permitted_complete_ : any_complete_) {
return SearchBreadcrumbs(search_for, permitted, chain);
}
}
{
std::unique_lock<std::shared_mutex> write_lock(lock_);
if (!(permitted ? permitted_complete_ : any_complete_)) {
PerformDependencyWalk(permitted);
}
}
std::shared_lock<std::shared_mutex> read_lock(lock_);
return SearchBreadcrumbs(search_for, permitted, chain);
}
bool HeaderChecker::ReachabilityCache::SearchBreadcrumbs(
const Target* search_for,
bool permitted,
Chain* chain) const {
const BreadcrumbTable& breadcrumbs =
permitted ? permitted_breadcrumbs_ : any_breadcrumbs_;
ChainLink incoming_link = breadcrumbs.GetLink(search_for);
if (!incoming_link.target)
return false;
chain->clear();
const Target* cur = search_for;
while (cur != source_target_) {
ChainLink link = breadcrumbs.GetLink(cur);
chain->push_back(ChainLink(cur, link.is_public));
cur = link.target;
}
chain->push_back(ChainLink(source_target_, true));
return true;
}
bool HeaderChecker::CheckFile(const std::vector<const Target*>& targets,
const SourceFile& file,
std::vector<Err>* errors) const {
ScopedTrace trace(TraceItem::TRACE_CHECK_HEADER, file.value());
if (!check_generated_ && IsFileInOuputDir(file))
return true;
base::FilePath path = build_settings_->GetFullPath(file);
std::string contents;
if (!base::ReadFileToString(path, &contents)) {
if (IsFileInOuputDir(file))
return true;
for (const Target* from_target : targets) {
errors->emplace_back(
from_target->defined_from(), "Source file not found.",
"The target:\n " + from_target->label().GetUserVisibleName(false) +
"\nhas a source file:\n " + file.value() +
"\nwhich was not found.");
}
return false;
}
InputFile input_file(file);
input_file.SetContents(contents);
std::vector<IncludeStringWithLocation> includes;
CIncludeIterator iter(&input_file);
IncludeStringWithLocation include;
while (iter.GetNextIncludeString(&include)) {
if (include.system_style_include && !check_system_)
continue;
includes.push_back(include);
}
if (includes.empty())
return true;
size_t error_count_before = errors->size();
for (const Target* from_target : targets) {
std::vector<SourceDir> include_dirs;
for (ConfigValuesIterator target_iter(from_target); !target_iter.done();
target_iter.Next()) {
const std::vector<SourceDir>& target_include_dirs =
target_iter.cur().include_dirs();
include_dirs.insert(include_dirs.end(), target_include_dirs.begin(),
target_include_dirs.end());
}
ReachabilityCache& from_target_cache =
GetReachabilityCacheForTarget(from_target);
for (const auto& inc : includes) {
Err err;
SourceFile included_file =
SourceFileForInclude(inc, include_dirs, input_file, &err);
if (!included_file.is_null()) {
CheckInclude(from_target_cache, input_file, included_file, inc.location,
errors);
}
}
}
return errors->size() == error_count_before;
}
void HeaderChecker::CheckInclude(ReachabilityCache& from_target_cache,
const InputFile& source_file,
const SourceFile& include_file,
const LocationRange& range,
std::vector<Err>* errors) const {
FileMap::const_iterator found = file_map_.find(include_file);
if (found == file_map_.end())
return;
const TargetVector& targets = found->second;
Chain chain;
const Target* from_target = from_target_cache.source_target();
bool present_in_current_toolchain = false;
for (const auto& target : targets) {
if (from_target->label().ToolchainsEqual(target.target->label())) {
present_in_current_toolchain = true;
break;
}
}
if (!present_in_current_toolchain)
return;
Err last_error;
bool found_dependency = false;
for (const auto& target : targets) {
const Target* to_target = target.target;
if (to_target == from_target)
return;
bool is_permitted_chain = false;
if (IsDependencyOf(to_target, from_target_cache, &chain,
&is_permitted_chain)) {
DCHECK(chain.size() >= 2);
DCHECK(chain[0].target == to_target);
DCHECK(chain[chain.size() - 1].target == from_target);
found_dependency = true;
bool effectively_public =
target.is_public || FriendMatches(to_target, from_target);
if (effectively_public && is_permitted_chain) {
last_error = Err();
break;
}
if (!effectively_public) {
last_error = Err(CreatePersistentRange(source_file, range),
"Including a private header.",
"This file is private to the target " +
target.target->label().GetUserVisibleName(false));
} else if (!is_permitted_chain) {
last_error = Err(CreatePersistentRange(source_file, range),
"Can't include this header from here.",
GetDependencyChainPublicError(chain));
} else {
NOTREACHED();
}
} else if (to_target->allow_circular_includes_from().find(
from_target->label()) !=
to_target->allow_circular_includes_from().end()) {
found_dependency = true;
last_error = Err();
break;
}
}
if (!found_dependency || last_error.has_error()) {
if (!found_dependency) {
DCHECK(!last_error.has_error());
Err err = MakeUnreachableError(source_file, range, from_target, targets);
errors->push_back(std::move(err));
} else {
errors->push_back(std::move(last_error));
}
return;
}
}
HeaderChecker::ReachabilityCache& HeaderChecker::GetReachabilityCacheForTarget(
const Target* target) const {
size_t shard_index = target->label().hash() % kNumShards;
auto& shard = dependency_cache_[shard_index];
std::unique_lock<std::shared_mutex> lock(shard.lock);
auto it = shard.cache.find(target);
if (it == shard.cache.end()) {
it =
shard.cache.emplace(target, std::make_unique<ReachabilityCache>(target))
.first;
}
return *it->second;
}
bool HeaderChecker::IsDependencyOf(const Target* search_for,
ReachabilityCache& from_target_cache,
Chain* chain,
bool* is_permitted) const {
const Target* search_from = from_target_cache.source_target();
if (search_for == search_from) {
*is_permitted = true;
return false;
}
if (from_target_cache.SearchForDependencyTo(search_for, true, chain)) {
*is_permitted = true;
return true;
}
if (from_target_cache.SearchForDependencyTo(search_for, false, chain)) {
*is_permitted = false;
return true;
}
return false;
}
Err HeaderChecker::MakeUnreachableError(const InputFile& source_file,
const LocationRange& range,
const Target* from_target,
const TargetVector& targets) {
std::vector<const Target*> targets_with_matching_toolchains;
std::vector<const Target*> targets_with_other_toolchains;
for (const TargetInfo& candidate : targets) {
if (candidate.target->toolchain() == from_target->toolchain())
targets_with_matching_toolchains.push_back(candidate.target);
else
targets_with_other_toolchains.push_back(candidate.target);
}
for (int other_index = 0;
other_index < static_cast<int>(targets_with_other_toolchains.size());
other_index++) {
for (const Target* cur_matching : targets_with_matching_toolchains) {
if (TargetLabelsMatchExceptToolchain(
cur_matching, targets_with_other_toolchains[other_index])) {
targets_with_other_toolchains.erase(
targets_with_other_toolchains.begin() + other_index);
other_index--;
break;
}
}
}
bool include_toolchain = !targets_with_other_toolchains.empty();
std::string msg = "It is not in any dependency of\n " +
from_target->label().GetUserVisibleName(include_toolchain);
msg += "\nThe include file is in the target(s):\n";
for (auto* target : targets_with_matching_toolchains)
msg += " " + target->label().GetUserVisibleName(include_toolchain) + "\n";
for (auto* target : targets_with_other_toolchains)
msg += " " + target->label().GetUserVisibleName(include_toolchain) + "\n";
if (targets_with_other_toolchains.size() +
targets_with_matching_toolchains.size() >
1)
msg += "at least one of ";
msg += "which should somehow be reachable.\n";
msg +=
"This might be a false alarm if you are using conditional include. "
"See \"gn help nogncheck\".";
return Err(CreatePersistentRange(source_file, range), "Include not allowed.",
msg);
}