| // Copyright 2014 The Chromium 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/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" | |
| // This class includes a list of all known files in the build and which targets | |
| // they are in. When CheckFile is called, it identifies which targets the file | |
| // is in. If it is in at least one binary target (like a static or shared | |
| // library, but not a config or an action), then the toolchain is following | |
| // the rules. | |
| // | |
| // The rules are: | |
| // 1. A target lists a header as public (the default) or private (detected | |
| // properly). | |
| // | |
| // 2. A target lists a header as private. Then another target depends on the | |
| // first target. The second target cannot include the private header. | |
| // This is the more advanced check. | |
| // | |
| // 3. A target lists a header as public (the default). Then another target | |
| // depends on the first target. The second target can include the public | |
| // header. | |
| // | |
| // 4. A target lists a header. Another target that does not depend on the | |
| // first target cannot include the header. | |
| // | |
| namespace { | |
| struct PublicGeneratedPair { | |
| PublicGeneratedPair() : is_public(false), is_generated(false) {} | |
| bool is_public; | |
| bool is_generated; | |
| }; | |
| // This class makes InputFiles on the stack as it reads files to check. When | |
| // we throw an error, the Err indicates a locatin which has a pointer to | |
| // an InputFile that must persist as long as the Err does. | |
| // | |
| // To make this work, this function creates a clone of the InputFile managed | |
| // by the InputFileManager so the error can refer to something that | |
| // persists. This means that the current file contents will live as long as | |
| // the program, but this is OK since we're erroring out anyway. | |
| LocationRange CreatePersistentRange(const InputFile& input_file, | |
| const LocationRange& range) { | |
| InputFile* clone_input_file; | |
| std::vector<Token>* tokens; // Don't care about this. | |
| std::unique_ptr<ParseNode>* parse_root; // Don't care about this. | |
| 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())); | |
| } | |
| // 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 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"; | |
| // Invalid chains should always be 0 (no chain) or more than two | |
| // (intermediate private dependencies). 1 and 2 are impossible because a | |
| // target can always include headers from itself and its direct dependents. | |
| DCHECK(chain.size() != 1 && chain.size() != 2); | |
| if (chain.empty()) { | |
| ret += "There is no dependency chain between these targets."; | |
| } 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_target) { | |
| ret += | |
| "\nPlease depend directly on the destination target.\n" | |
| "Including headers from the public_deps of " + | |
| strict_target->label().GetUserVisibleName(false) + | |
| " is blocked because check_includes_strict = true is enabled on it.\n" | |
| "check_includes_strict = true is highly recommended for all new code " | |
| "to ensure a correct build graph.\n"; | |
| } 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"; | |
| } | |
| ret += "\nDependency 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) { | |
| // Identify private dependencies so the user can see where in the | |
| // dependency chain things went bad. Don't list this for the first link | |
| // in the chain since direct dependencies are OK, and listing that as | |
| // "private" may make people feel like they need to fix it. | |
| if (i == static_cast<int>(chain.size()) - 1 || chain[i - 1].is_public) { | |
| if (i != static_cast<int>(chain.size()) - 1 && | |
| chain[i].target->check_includes_strict()) { | |
| ret.append(" --[check_includes_strict = true]-->"); | |
| } else { | |
| ret.append(" -->"); | |
| } | |
| } else { | |
| ret.append(" --[private]-->"); | |
| } | |
| } | |
| ret.append("\n"); | |
| } | |
| } | |
| return ret; | |
| } | |
| // Returns true if the two targets have the same label not counting the | |
| // toolchain. | |
| bool TargetLabelsMatchExceptToolchain(const Target* a, const Target* b) { | |
| return a->label().dir() == b->label().dir() && | |
| a->label().name() == b->label().name(); | |
| } | |
| // Returns true if the target |annotation_on| includes a friend annotation | |
| // that allows |is_marked_friend| as a friend. | |
| bool FriendMatches(const Target* annotation_on, | |
| const Target* is_marked_friend) { | |
| return LabelPattern::VectorMatches(annotation_on->friends(), | |
| is_marked_friend->label()); | |
| } | |
| } // namespace | |
| 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<Violation>* violations) { | |
| FileMap files_to_check; | |
| for (auto* check : to_check) { | |
| // This function will get called with all target types, but check only | |
| // applies to binary targets. | |
| if (check->IsBinary()) | |
| AddTargetToFileMap(check, &files_to_check); | |
| } | |
| WorkerPool pool; | |
| { | |
| ScopedTrace precompute_trace(TraceItem::TRACE_CHECK_HEADERS, | |
| "Precompute reachability"); | |
| std::set<const Target*> targets_to_precompute; | |
| for (const auto& file : files_to_check) { | |
| for (const auto& target_info : file.second) { | |
| if (target_info.target->check_includes()) | |
| targets_to_precompute.insert(target_info.target); | |
| } | |
| } | |
| if (!targets_to_precompute.empty()) { | |
| task_count_.Increment(); | |
| for (const auto* target : targets_to_precompute) { | |
| task_count_.Increment(); | |
| pool.PostTask([this, target]() { | |
| ReachabilityCache& cache = GetReachabilityCacheForTarget(target); | |
| cache.PerformDependencyWalk(true); | |
| cache.PerformDependencyWalk(false); | |
| if (!task_count_.Decrement()) { | |
| std::unique_lock<std::mutex> lock(task_count_lock_); | |
| task_count_cv_.notify_one(); | |
| } | |
| }); | |
| } | |
| if (!task_count_.Decrement()) { | |
| std::unique_lock<std::mutex> lock(task_count_lock_); | |
| task_count_cv_.notify_one(); | |
| } | |
| std::unique_lock<std::mutex> lock(task_count_lock_); | |
| while (!task_count_.IsZero()) | |
| task_count_cv_.wait(lock); | |
| } | |
| } | |
| RunCheckOverFiles(files_to_check, force_check, &pool); | |
| if (violations_.empty()) | |
| return true; | |
| if (violations) | |
| *violations = violations_; | |
| return false; | |
| } | |
| void HeaderChecker::RunCheckOverFiles(const FileMap& files, | |
| bool force_check, | |
| WorkerPool* pool) { | |
| task_count_.Increment(); | |
| for (const auto& file : files) { | |
| // Only check C-like source files (RC files also have includes). | |
| 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_) { | |
| // If any target marks it as generated, don't check it. We have to check | |
| // file_map_, which includes all known files; files only includes those | |
| // being checked. | |
| bool is_generated = false; | |
| for (const auto& vect_i : file_map_[file.first]) | |
| is_generated |= vect_i.is_generated; | |
| if (is_generated) | |
| continue; | |
| } | |
| TargetVector targets_to_check; | |
| for (const auto& vect_i : file.second) { | |
| if (vect_i.target->check_includes()) { | |
| targets_to_check.push_back(vect_i); | |
| } | |
| } | |
| if (targets_to_check.empty()) | |
| continue; | |
| task_count_.Increment(); | |
| pool->PostTask([this, targets = std::move(targets_to_check), | |
| file = file.first]() { DoWork(targets, file); }); | |
| } | |
| if (!task_count_.Decrement()) { | |
| std::unique_lock<std::mutex> lock(task_count_lock_); | |
| task_count_cv_.notify_one(); | |
| } | |
| // Wait for all tasks posted by this method to complete. | |
| std::unique_lock<std::mutex> auto_lock(task_count_lock_); | |
| while (!task_count_.IsZero()) | |
| task_count_cv_.wait(auto_lock); | |
| } | |
| void HeaderChecker::DoWork(const TargetVector& targets, | |
| const SourceFile& file) { | |
| std::vector<Violation> violations; | |
| if (!CheckFile(targets, file, &violations)) { | |
| std::lock_guard<std::mutex> lock(errors_lock_); | |
| violations_.insert(violations_.end(), | |
| std::make_move_iterator(violations.begin()), | |
| std::make_move_iterator(violations.end())); | |
| } | |
| if (!task_count_.Decrement()) { | |
| // Signal |task_count_cv_| when |task_count_| becomes zero. | |
| std::unique_lock<std::mutex> auto_lock(task_count_lock_); | |
| task_count_cv_.notify_one(); | |
| } | |
| } | |
| // static | |
| void HeaderChecker::AddTargetToFileMap(const Target* target, FileMap* dest) { | |
| // Files in the sources have this public bit by default. | |
| bool default_public = target->all_headers_public(); | |
| std::map<SourceFile, PublicGeneratedPair> files_to_public; | |
| // First collect the normal files, they get the default visibility. If you | |
| // depend on the compiled target, it should be enough to be able to include | |
| // the header. | |
| for (const auto& source : target->sources()) { | |
| files_to_public[source].is_public = default_public; | |
| } | |
| // Add in the public files, forcing them to public. This may overwrite some | |
| // entries, and it may add new ones. | |
| if (default_public) // List only used when default is not public. | |
| DCHECK(target->public_headers().empty()); | |
| for (const auto& source : target->public_headers()) { | |
| files_to_public[source].is_public = true; | |
| } | |
| // If target generates a swiftmodule, then | |
| // - it may use a bridge header which has default visibility | |
| // - it may generate public header which must be considered public | |
| 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; | |
| } | |
| } | |
| } | |
| // Add in outputs from actions. These are treated as public (since if other | |
| // targets can't use them, then there wouldn't be any point in outputting). | |
| 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; | |
| } | |
| // Add the merged list to the master list of all files. | |
| 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) { | |
| // Conduct the actual BFS. | |
| if (permitted ? permitted_complete_.load(std::memory_order_relaxed) | |
| : any_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 | |
| // time, it is added to work_queue and a "breadcrumb" is added, indicating | |
| // 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); | |
| while (!work_queue.empty()) { | |
| const Target* target = work_queue.front(); | |
| work_queue.pop(); | |
| if (permitted && target != source_target_ && | |
| target->check_includes_strict()) { | |
| continue; | |
| } | |
| for (const auto& dep : target->public_deps()) { | |
| if (breadcrumbs.Insert(dep.ptr, target, true)) | |
| work_queue.push(dep.ptr); | |
| } | |
| if (!permitted || 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)) | |
| work_queue.push(dep.ptr); | |
| } | |
| } | |
| } | |
| if (permitted) { | |
| permitted_complete_.store(true, std::memory_order_release); | |
| } else { | |
| any_complete_.store(true, std::memory_order_release); | |
| } | |
| } | |
| bool HeaderChecker::ReachabilityCache::SearchForDependencyTo( | |
| const Target* search_for, | |
| bool permitted, | |
| Chain* chain) { | |
| if (permitted ? permitted_complete_.load(std::memory_order_acquire) | |
| : any_complete_.load(std::memory_order_acquire)) { | |
| return SearchBreadcrumbs(search_for, permitted, 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); | |
| } | |
| } | |
| 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; | |
| // Found it! Reconstruct the chain. | |
| 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 TargetVector& targets, | |
| const SourceFile& file, | |
| std::vector<Violation>* violations) const { | |
| ScopedTrace trace(TraceItem::TRACE_CHECK_HEADER, file.value()); | |
| // Sometimes you have generated source files included as sources in another | |
| // target. These won't exist at checking time. Since we require all generated | |
| // files to be somewhere in the output tree, we can just check the name to | |
| // see if they should be skipped. | |
| if (!check_generated_ && IsFileInOuputDir(file)) | |
| return true; | |
| base::FilePath path = build_settings_->GetFullPath(file); | |
| std::string contents; | |
| if (!base::ReadFileToString(path, &contents)) { | |
| // A missing (not yet) generated file is an acceptable problem | |
| // considering this code does not understand conditional includes. | |
| if (IsFileInOuputDir(file)) | |
| return true; | |
| for (const TargetInfo& from_target_info : targets) { | |
| const Target* from_target = from_target_info.target; | |
| violations->emplace_back( | |
| Err(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."), | |
| file, SourceFile()); | |
| } | |
| 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 violations_count_before = violations->size(); | |
| for (const TargetInfo& from_target_info : targets) { | |
| const Target* from_target = from_target_info.target; | |
| 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()) { | |
| std::vector<Err> include_errors; | |
| CheckInclude(from_target_cache, | |
| from_target_info.is_public && | |
| file.GetType() == SourceFile::SOURCE_H, | |
| input_file, included_file, inc.location, &include_errors); | |
| for (auto& e : include_errors) { | |
| violations->emplace_back(std::move(e), file, | |
| std::move(included_file)); | |
| } | |
| } | |
| } | |
| } | |
| return violations->size() == violations_count_before; | |
| } | |
| // If the file exists: | |
| // - The header must be in the public section of a target, or it must | |
| // be in the sources with no public list (everything is implicitly public). | |
| // - The dependency path to the included target must follow only public_deps. | |
| // - If there are multiple targets with the header in it, only one need be | |
| // valid for the check to pass. | |
| void HeaderChecker::CheckInclude(ReachabilityCache& from_target_cache, | |
| bool is_public_header, | |
| const InputFile& source_file, | |
| const SourceFile& include_file, | |
| const LocationRange& range, | |
| std::vector<Err>* errors) const { | |
| // Assume if the file isn't declared in our sources that we don't need to | |
| // check it. It would be nice if we could give an error if this happens, but | |
| // our include finder is too primitive and returns all includes, even if | |
| // they're in a #if not executed in the current build. In that case, it's | |
| // not unusual for the buildfiles to not specify that header at all. | |
| FileMap::const_iterator found = file_map_.find(include_file); | |
| if (found == file_map_.end()) | |
| return; | |
| const TargetVector& targets = found->second; | |
| Chain chain; // Prevent reallocating in the loop. | |
| const Target* from_target = from_target_cache.source_target(); | |
| // If the file is unknown in the current toolchain (rather than being private | |
| // or in a target not visible to the current target), ignore it. This is a | |
| // bit of a hack to account for the fact that the include finder doesn't | |
| // understand the preprocessor. | |
| // | |
| // When not cross-compiling, if a platform specific header is conditionally | |
| // included in the build, and preprocessor conditions around #includes of | |
| // that match the build conditions, everything will be OK because the file | |
| // won't be known to GN even though the #include finder identified the file. | |
| // | |
| // Cross-compiling breaks this. When compiling Android on Linux, for example, | |
| // we might see both Linux and Android definitions of a target and know | |
| // about the union of all headers in the build. Since the #include finder | |
| // ignores preprocessor, we will find the Linux headers in the Android | |
| // build and note that a dependency from the Android target to the Linux | |
| // one is missing (these might even be the same target in different | |
| // toolchains!). | |
| 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; | |
| // For all targets containing this file, we require that at least one be | |
| // a direct or public dependency of the current target, and either (1) the | |
| // header is public within the target, or (2) there is a friend definition | |
| // allowlisting the includor. | |
| // | |
| // If there is more than one target containing this header, we may encounter | |
| // some error cases before finding a good one. This error stores the previous | |
| // one encountered, which we may or may not throw away. | |
| Err last_error; | |
| bool found_dependency = false; | |
| for (const auto& target : targets) { | |
| // We always allow source files in a target to include headers also in that | |
| // target, unless strict checking is enabled and a public header includes | |
| // a private header. | |
| const Target* to_target = target.target; | |
| if (to_target == from_target) { | |
| if (from_target->check_includes_strict() && is_public_header && | |
| !target.is_public) { | |
| last_error = Err( | |
| CreatePersistentRange(source_file, range), | |
| "Public headers cannot include private headers of the same target.", | |
| "The public header:\n " + source_file.name().value() + | |
| "\nis including a private header of the same target:\n " + | |
| include_file.value() + | |
| "\nEither make the included header public, make the includer " | |
| "private,\n" | |
| "or make a source_set containing public = [private_headers] " | |
| "and add it to public_deps."); | |
| errors->push_back(std::move(last_error)); | |
| } | |
| 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) { | |
| if (from_target->check_includes_strict() && is_public_header && | |
| !chain[chain.size() - 2].is_public) { | |
| last_error = Err( | |
| CreatePersistentRange(source_file, range), | |
| "Public headers cannot include private dependencies.", | |
| "The public header:\n " + source_file.name().value() + | |
| "\nis including a header from private dependency:\n " + | |
| to_target->label().GetUserVisibleName(false) + | |
| "\nEither move the dependency to public_deps, or make this " | |
| "header private."); | |
| continue; | |
| } | |
| // This one is OK, we're done. | |
| last_error = Err(); | |
| break; | |
| } | |
| // Diagnose the error. | |
| if (!effectively_public) { | |
| // Danger: must call CreatePersistentRange to put in Err. | |
| 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()) { | |
| // Not a dependency, but this include is allowlisted from the destination. | |
| 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 { | |
| // Found at least one dependency chain above, but it had an error. | |
| errors->push_back(std::move(last_error)); | |
| } | |
| return; | |
| } | |
| // One thing we didn't check for is targets that expose their dependents | |
| // headers in their own public headers. | |
| // | |
| // Say we have A -> B -> C. If C has public_configs, everybody getting headers | |
| // from C should get the configs also or things could be out-of-sync. Above, | |
| // we check for A including C's headers directly, but A could also include a | |
| // header from B that in turn includes a header from C. | |
| // | |
| // There are two ways to solve this: | |
| // - If a public header in B includes C, force B to publicly depend on C. | |
| // This is possible to check, but might be super annoying because most | |
| // targets (especially large leaf-node targets) don't declare | |
| // public/private headers and you'll get lots of false positives. | |
| // | |
| // - Save the includes found in each file and actually compute the graph of | |
| // includes to detect when A implicitly includes C's header. This will not | |
| // have the annoying false positive problem, but is complex to write. | |
| } | |
| 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) { | |
| // A target is always visible from itself. | |
| *is_permitted = true; | |
| return false; | |
| } | |
| // 1. Try permitted dependency. | |
| if (from_target_cache.SearchForDependencyTo(search_for, true, chain)) { | |
| *is_permitted = true; | |
| return true; | |
| } | |
| // 2. Try any dependency. | |
| 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) { | |
| // Normally the toolchains will all match, but when cross-compiling, we can | |
| // get targets with more than one toolchain in the list of possibilities. | |
| 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); | |
| } | |
| // It's common when cross-compiling to have a target with the same file in | |
| // more than one toolchain. We could output all of them, but this is | |
| // generally confusing to people (most end-users won't understand toolchains | |
| // well). | |
| // | |
| // So delete any candidates in other toolchains that also appear in the same | |
| // toolchain as the from_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])) { | |
| // Found a duplicate, erase it. | |
| targets_with_other_toolchains.erase( | |
| targets_with_other_toolchains.begin() + other_index); | |
| other_index--; | |
| break; | |
| } | |
| } | |
| } | |
| // Only display toolchains on labels if they don't all match. | |
| 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\"."; | |
| // Danger: must call CreatePersistentRange to put in Err. | |
| return Err(CreatePersistentRange(source_file, range), "Include not allowed.", | |
| msg); | |
| } |