blob: de1980e061f619236b26018460b22c430b2e3961 [file] [log] [blame]
/*
* Copyright 2015 The Kythe Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "kythe/cxx/tools/fyi/fyi.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "clang/Frontend/ASTUnit.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/TextDiagnosticPrinter.h"
#include "clang/Lex/Preprocessor.h"
#include "clang/Parse/ParseAST.h"
#include "clang/Rewrite/Core/Rewriter.h"
#include "clang/Sema/ExternalSemaSource.h"
#include "clang/Sema/Sema.h"
#include "kythe/cxx/common/kythe_uri.h"
#include "kythe/cxx/common/schema/edges.h"
#include "kythe/cxx/common/schema/facts.h"
#include "kythe/cxx/indexer/cxx/proto_conversions.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/Timer.h"
#include "llvm/Support/raw_ostream.h"
#include "third_party/llvm/src/clang_builtin_headers.h"
namespace kythe {
namespace fyi {
/// \brief Tracks changes and edits to a single file identified by its full
/// path.
///
/// Holds a `llvm::MemoryBuffer` with the results of the most recent edits
/// if edits have been made.
///
/// This object is first created when the compiler enters a new main source
/// file. Before each new compile or reparse pass, the outer loop should call
/// ::BeginPass(). FileTracker is then notified of various events involving the
/// file being processed. If the FileTracker enters a state that is not kBusy,
/// it can make no further progress. Otherwise, when the compile completes, it
/// is expected that the outer loop will attempt a call to ::Rewrite() with a
/// fresh Rewriter instance. If that succeeds, then ::CommitRewrite will update
/// internal buffers for subsequent passes.
class FileTracker {
public:
explicit FileTracker(llvm::StringRef filename) : filename_(filename) {}
/// \brief Returns the current rewritten file (or null, if rewriting hasn't
/// happend).
///
/// The object shares its lifetime with this FileTracker.
llvm::MemoryBuffer* memory_buffer() { return memory_buffer_.get(); }
llvm::StringRef filename() { return filename_; }
const llvm::StringRef backing_store() {
assert(active_buffer_ < 2);
if (memory_buffer_backing_store_[active_buffer_].empty()) {
return "";
}
auto* store = &memory_buffer_backing_store_[active_buffer_];
const char* start = store->data();
// Why the - 1?: MemoryBufferBackingStore ends with a NUL terminator.
return llvm::StringRef(start, store->size() - 1);
}
/// \param Start a new pass involving this `FileTracker`
void BeginPass() {
file_begin_ = clang::SourceLocation();
pass_had_errors_ = false;
}
/// Called for each include file we discover is in the file during a major
/// pass.
/// Will catch includes that we've added in earlier major passes as well.
/// \param source_manager the active SourceManager
/// \param canonical_path the canonical path to the include file
/// \param uttered_path the path as it appeared in the program
/// \param is_angled whether angle brackets were used
/// \param hash_location the source location of the include's \#
/// \param end_location the source location following the include
void NextInclude(clang::SourceManager* source_manager,
llvm::StringRef canonical_path, llvm::StringRef uttered_path,
bool IsAngled, clang::SourceLocation hash_location,
clang::SourceLocation end_location) {
unsigned offset = source_manager->getFileOffset(end_location);
if (offset > last_include_offset_) {
last_include_offset_ = offset;
}
}
/// \brief Rewrite the associated source file with our tentative suggestions.
/// \param rewriter a valid Rewriter.
/// \return true if changes will be made, false otherwise.
bool Rewrite(clang::Rewriter* rewriter) {
if (state_ != State::kBusy) {
return false;
}
if (!pass_had_errors_) {
state_ = State::kSuccess;
return false;
}
if (!untried_.empty()) {
auto to_try = *untried_.begin();
untried_.erase(untried_.begin());
tried_.insert(to_try);
rewriter->InsertTextAfter(
file_begin_.getLocWithOffset(last_include_offset_),
"\n#include \"" + to_try + "\"\n");
return true;
}
// We have nothing to do, so abort.
state_ = State::kFailure;
return false;
}
/// \brief Rewrite the old file into a new file, discarding any previously
/// allocated buffers.
/// \param file_id the current ID of the file we are rewriting
/// \param rewriter a valid Rewriter.
void CommitRewrite(clang::FileID file_id, clang::Rewriter* rewriter) {
assert(active_buffer_ < 2);
can_undo_ = true;
active_buffer_ = 1 - active_buffer_;
auto* store = &memory_buffer_backing_store_[active_buffer_];
const clang::RewriteBuffer* buffer = rewriter->getRewriteBufferFor(file_id);
store->clear();
llvm::raw_svector_ostream buffer_stream(*store);
buffer->write(buffer_stream);
// Required null terminator.
store->push_back(0);
const char* start = store->data();
llvm::StringRef data(start, store->size() - 1);
memory_buffer_ = llvm::MemoryBuffer::getMemBuffer(data);
}
/// \brief Analysis state, maintained across passes.
enum class State {
kBusy, ///< We are trying to repair this file.
kSuccess, ///< We have repaired this file (or there is nothing we can do).
kFailure ///< We are no longer trying to repair this file.
};
/// \brief Gets the state (busy, OK, or bad) of this FileTracker.
State state() const { return state_; }
/// \brief Marks that this FileTracker cannot be repaired.
void mark_failed() { state_ = State::kFailure; }
/// \brief Gets the location at the very top of the file (in this pass).
clang::SourceLocation file_begin() const { return file_begin_; }
/// \brief Sets the location at the very top of the file (in this pass).
void set_file_begin(clang::SourceLocation location) {
file_begin_ = location;
}
/// \brief Decode and possibly take action on a diagnostic received during
/// a compilation (sub)pass.
/// \param diagnostic The diagnostic to handle.
void HandleStoredDiagnostic(clang::StoredDiagnostic& diagnostic) {
pass_had_errors_ = true;
}
/// \brief Add an include to the set of includes to try.
/// \param include_path The include path to try (as a quoted include).
void TryInclude(const std::string& include_path) {
if (!tried_.count(include_path)) {
untried_.insert(include_path);
}
}
/// \brief Record the initial state of the file before rewriting it.
/// \param content The content of the file.
void SetInitialContent(llvm::StringRef content) {
if (!saw_initial_state_) {
active_buffer_ = 0;
memory_buffer_backing_store_[0].clear();
memory_buffer_backing_store_[0].append(content.begin(), content.end());
memory_buffer_backing_store_[0].push_back(0);
memory_buffer_ = llvm::MemoryBuffer::getMemBuffer(backing_store());
can_undo_ = false;
saw_initial_state_ = true;
}
}
private:
friend class Action;
/// Try to undo the previous change to the backing store.
bool Undo() {
assert(active_buffer_ < 2);
if (can_undo_) {
active_buffer_ = 1 - active_buffer_;
memory_buffer_ = llvm::MemoryBuffer::getMemBuffer(backing_store());
can_undo_ = false;
return true;
}
return false;
}
/// The absolute path to the file this FileTracker tracks. Used as a key
/// to connect between passes.
std::string filename_;
/// The location of the beginning of the tracked file. This changes after
/// each pass.
clang::SourceLocation file_begin_;
/// The offset of the last include in the original source file. This will
/// be used as the insertion point for new include directives.
unsigned last_include_offset_ = 0;
/// If this file has been modified, points to a MemoryBuffer containing
/// the full text of the modified file.
std::unique_ptr<llvm::MemoryBuffer> memory_buffer_ = nullptr;
/// Data backing the MemoryBuffer. This is double-buffered, allowing for one
/// step of undo. `active_buffer_` selects which buffer we should read from.
llvm::SmallVector<char, 128> memory_buffer_backing_store_[2];
/// Which backing store is currently active and which is the backup.
/// Always < 2.
size_t active_buffer_ = 0;
/// Can we undo the previous move?
bool can_undo_ = false;
/// Have we ever seen the initial state of the file?
bool saw_initial_state_ = false;
/// The current of this FileTracker independent of pass.
State state_ = State::kBusy;
/// True if the last subpass had (recoverable) errors.
bool pass_had_errors_ = false;
/// Includes we've already tried.
std::set<std::string> tried_;
/// Includes we have left to try.
std::set<std::string> untried_;
};
/// \brief During non-reparse passes, PreprocessorHooks listens for events
/// indicating the files being analyzed and their preprocessor directives.
class PreprocessorHooks : public clang::PPCallbacks {
public:
/// \param enclosing_pass The `Action` controlling this pass. Not owned.
explicit PreprocessorHooks(Action* enclosing_pass)
: enclosing_pass_(enclosing_pass), tracked_file_(nullptr) {}
/// \copydoc PPCallbacks::FileChanged
///
/// Finds the `FileEntry` and starting `SourceLocation` for each tracked
/// file on every pass.
void FileChanged(clang::SourceLocation loc,
clang::PPCallbacks::FileChangeReason reason,
clang::SrcMgr::CharacteristicKind file_type,
clang::FileID prev_fid) override;
/// \copydoc PPCallbacks::InclusionDirective
///
/// When \p SourceFile is the file being tracked by the enclosing pass,
/// records details about each inclusion directive encountered (such as
/// the name of the included file, the location of the directive, and so on).
void InclusionDirective(clang::SourceLocation hash_location,
const clang::Token& include_token,
llvm::StringRef file_name, bool is_angled,
clang::CharSourceRange file_name_range,
const clang::FileEntry* include_file,
llvm::StringRef search_path,
llvm::StringRef relative_path,
const clang::Module* imported,
clang::SrcMgr::CharacteristicKind FileType) override;
private:
friend class Action;
/// The current `Action`. Not owned.
Action* enclosing_pass_;
/// The `FileEntry` corresponding to the tracker in `enclosing_pass_`.
/// Not owned.
const clang::FileEntry* tracked_file_;
};
/// \brief Manages a full parse and any subsequent reparses for a single file.
class Action : public clang::ASTFrontendAction,
public clang::ExternalSemaSource {
public:
explicit Action(ActionFactory& factory) : factory_(factory) {}
/// \copydoc ASTFrontendAction::BeginInvocation
bool BeginInvocation(clang::CompilerInstance& CI) override {
auto* pp_opts = &CI.getPreprocessorOpts();
pp_opts->RetainRemappedFileBuffers = true;
pp_opts->AllowPCHWithCompilerErrors = true;
factory_.RemapFiles(CI.getHeaderSearchOpts().ResourceDir,
&pp_opts->RemappedFileBuffers);
return true;
}
/// \copydoc ASTFrontendAction::CreateASTConsumer
std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(
clang::CompilerInstance& compiler, llvm::StringRef in_file) override {
tracker_ = factory_.GetOrCreateTracker(in_file);
// Don't bother starting a new pass if the tracker is finished.
if (tracker_->state() == FileTracker::State::kBusy) {
tracker_->BeginPass();
compiler.getPreprocessor().addPPCallbacks(
llvm::make_unique<PreprocessorHooks>(this));
}
return llvm::make_unique<clang::ASTConsumer>();
}
/// \copydoc ASTFrontendAction::ExecuteAction
void ExecuteAction() override {
// We have to reproduce what ASTFrontendAction::ExecuteAction does, since
// we have to attach ourselves as an ExternalSemaSource to Sema before
// calling ParseAST.
// Do nothing if we've already given up on or finished this file.
if (tracker_->state() != FileTracker::State::kBusy) {
return;
}
clang::CompilerInstance* compiler = &getCompilerInstance();
assert(!compiler->hasSema() && "CI already has Sema");
if (hasCodeCompletionSupport() &&
!compiler->getFrontendOpts().CodeCompletionAt.FileName.empty())
compiler->createCodeCompletionConsumer();
clang::CodeCompleteConsumer* completion_consumer = nullptr;
if (compiler->hasCodeCompletionConsumer())
completion_consumer = &compiler->getCodeCompletionConsumer();
compiler->createSema(getTranslationUnitKind(), completion_consumer);
compiler->getSema().addExternalSource(this);
clang::ParseAST(compiler->getSema(), compiler->getFrontendOpts().ShowStats,
compiler->getFrontendOpts().SkipFunctionBodies);
}
/// \brief Copies the tickets from `reply.edge_set` to `request.ticket`.
/// \return false if no tickets were copied
template <typename Reply, typename Request>
bool CopyTicketsFromEdgeSets(const Reply& reply, Request* request) {
for (const auto& edge_set : reply.edge_sets()) {
for (const auto& group : edge_set.second.groups()) {
for (const auto& edge : group.second.edge()) {
request->add_ticket(edge.target_ticket());
}
}
}
return request->ticket_size() != 0;
}
/// \brief Adds the paths of all /kythe/node/file nodes from `reply.node` to
/// this Action's `FileTracker`'s include list.
template <typename Reply>
void AddFileNodesToTracker(const Reply& reply) {
for (const auto& parent : reply.nodes()) {
bool is_file = false;
for (const auto& fact : parent.second.facts()) {
if (fact.first == kythe::common::schema::kFactNodeKind) {
is_file = (fact.second == "/kythe/node/file");
break;
}
}
if (!is_file) {
continue;
}
auto maybe_uri = URI::FromString(parent.first);
if (maybe_uri.first) {
tracker_->TryInclude(maybe_uri.second.v_name().path());
}
}
}
/// \copydoc ExternalSemaSource::CorrectTypo
clang::TypoCorrection CorrectTypo(
const clang::DeclarationNameInfo& typo, int lookup_kind,
clang::Scope* scope, clang::CXXScopeSpec* scope_spec,
clang::CorrectionCandidateCallback& callback,
clang::DeclContext* member_context, bool entering_context,
const clang::ObjCObjectPointerType* objc_ptr_type) override {
// Conservatively assume that something went wrong if we had to invoke
// typo correction.
tracker_->pass_had_errors_ = true;
// Look for any name nodes that could help.
proto::VName name_node;
name_node.set_signature(typo.getAsString() + "#n");
name_node.set_language("c++");
proto::EdgesRequest named_edges_request;
auto name_uri = URI(name_node).ToString();
named_edges_request.add_ticket(name_uri);
// We've found at least one interesting name in the graph. Now we need
// to figure out which nodes those names are bound to.
named_edges_request.add_kind(
absl::StrCat("%", kythe::common::schema::kNamed));
proto::EdgesReply named_edges_reply;
std::string error_text;
if (!factory_.xrefs_->Edges(named_edges_request, &named_edges_reply,
&error_text)) {
absl::FPrintF(stderr, "Xrefs error (named): %s\n", error_text);
return clang::TypoCorrection();
}
// Get information about the places where those nodes were defined.
proto::EdgesRequest defined_edges_request;
proto::EdgesReply defined_edges_reply;
if (!CopyTicketsFromEdgeSets(named_edges_reply, &defined_edges_request)) {
return clang::TypoCorrection();
}
defined_edges_request.add_kind(
ToStringRef(absl::StrCat("%", kythe::common::schema::kDefines)));
if (!factory_.xrefs_->Edges(defined_edges_request, &defined_edges_reply,
&error_text)) {
absl::FPrintF(stderr, "Xrefs error (defines): %s\n", error_text);
return clang::TypoCorrection();
}
// Finally, figure out whether we can make those definition sites visible
// to the site of the typo by adding an include.
proto::EdgesRequest childof_request;
proto::EdgesReply childof_reply;
if (!CopyTicketsFromEdgeSets(defined_edges_reply, &childof_request)) {
return clang::TypoCorrection();
}
childof_request.add_filter(kythe::common::schema::kFactNodeKind);
childof_request.add_kind(kythe::common::schema::kChildOf);
if (!factory_.xrefs_->Edges(childof_request, &childof_reply, &error_text)) {
absl::FPrintF(stderr, "Xrefs error (childof): %s\n", error_text);
return clang::TypoCorrection();
}
// Add those files to the set of includes to try out.
AddFileNodesToTracker(childof_reply);
return clang::TypoCorrection();
}
FileTracker* tracker() { return tracker_; }
private:
/// The `ActionFactory` orchestrating this multipass run.
ActionFactory& factory_;
/// The `FileTracker` keeping track of the file being processed.
FileTracker* tracker_ = nullptr;
};
void PreprocessorHooks::FileChanged(clang::SourceLocation loc,
clang::PPCallbacks::FileChangeReason reason,
clang::SrcMgr::CharacteristicKind file_type,
clang::FileID prev_fid) {
if (!enclosing_pass_) {
return;
}
if (reason == clang::PPCallbacks::EnterFile) {
clang::SourceManager* source_manager =
&enclosing_pass_->getCompilerInstance().getSourceManager();
clang::FileID loc_id = source_manager->getFileID(loc);
if (const clang::FileEntry* file_entry =
source_manager->getFileEntryForID(loc_id)) {
if (file_entry->getName() == enclosing_pass_->tracker()->filename()) {
enclosing_pass_->tracker()->set_file_begin(loc);
bool valid = true;
const auto* buffer =
source_manager->getMemoryBufferForFile(file_entry, &valid);
if (valid && buffer) {
enclosing_pass_->tracker()->SetInitialContent(buffer->getBuffer());
}
tracked_file_ = file_entry;
}
}
}
}
void PreprocessorHooks::InclusionDirective(
clang::SourceLocation hash_location, const clang::Token& include_token,
llvm::StringRef file_name, bool is_angled,
clang::CharSourceRange file_name_range,
const clang::FileEntry* include_file, llvm::StringRef search_path,
llvm::StringRef relative_path, const clang::Module* imported,
clang::SrcMgr::CharacteristicKind FileType) {
if (!enclosing_pass_ || !enclosing_pass_->tracker()) {
return;
}
clang::SourceManager* source_manager =
&enclosing_pass_->getCompilerInstance().getSourceManager();
auto id_position = source_manager->getDecomposedExpansionLoc(hash_location);
const auto* source_file =
source_manager->getFileEntryForID(id_position.first);
if (source_file == nullptr || include_file == nullptr) {
return;
}
if (tracked_file_ == source_file) {
enclosing_pass_->tracker()->NextInclude(
source_manager, include_file->getName(), file_name, is_angled,
hash_location, file_name_range.getEnd());
}
}
ActionFactory::ActionFactory(std::unique_ptr<XrefsClient> xrefs,
size_t iterations)
: xrefs_(std::move(xrefs)), iterations_(iterations) {
for (const auto* file = builtin_headers_create(); file->name; ++file) {
builtin_headers_.push_back(llvm::MemoryBuffer::getMemBufferCopy(
llvm::StringRef(file->data), file->name));
}
}
ActionFactory::~ActionFactory() {
for (auto& tracker : file_trackers_) {
delete tracker.second;
}
file_trackers_.clear();
}
void ActionFactory::RemapFiles(
llvm::StringRef resource_dir,
std::vector<std::pair<std::string, llvm::MemoryBuffer*>>*
remapped_buffers) {
remapped_buffers->clear();
for (FileTrackerMap::iterator I = file_trackers_.begin(),
E = file_trackers_.end();
I != E; ++I) {
FileTracker* tracker = I->second;
if (llvm::MemoryBuffer* buffer = tracker->memory_buffer()) {
remapped_buffers->push_back(std::make_pair(tracker->filename(), buffer));
}
}
for (const auto& buffer : builtin_headers_) {
llvm::SmallString<1024> out_path = resource_dir;
llvm::sys::path::append(out_path, "include");
llvm::sys::path::append(out_path, buffer->getBufferIdentifier());
remapped_buffers->push_back(std::make_pair(out_path.c_str(), buffer.get()));
}
}
FileTracker* ActionFactory::GetOrCreateTracker(llvm::StringRef filename) {
FileTrackerMap::iterator i = file_trackers_.find(filename);
if (i == file_trackers_.end()) {
FileTracker* new_tracker = new FileTracker(filename);
file_trackers_[filename] = new_tracker;
return new_tracker;
}
return i->second;
}
void ActionFactory::BeginNextIteration() {
assert(iterations_ > 0);
--iterations_;
}
bool ActionFactory::ShouldRunAgain() { return iterations_ > 0; }
bool ActionFactory::runInvocation(
std::shared_ptr<clang::CompilerInvocation> invocation,
clang::FileManager* files,
std::shared_ptr<clang::PCHContainerOperations> pch_container_ops,
clang::DiagnosticConsumer* diagnostics) {
// ASTUnit::LoadFromCompilerInvocationAction complains about this too, but
// we'll leave in our own assert to document the assumption.
assert(invocation->getFrontendOpts().Inputs.size() == 1);
llvm::IntrusiveRefCntPtr<clang::DiagnosticIDs> diag_ids(
new clang::DiagnosticIDs());
llvm::IntrusiveRefCntPtr<clang::DiagnosticsEngine> diags(
new clang::DiagnosticsEngine(diag_ids, &invocation->getDiagnosticOpts()));
if (diagnostics) {
diags->setClient(diagnostics, false);
} else {
diagnostics = new clang::TextDiagnosticPrinter(
llvm::errs(), &invocation->getDiagnosticOpts());
diags->setClient(diagnostics, /*ShouldOwnClient*/ true);
}
clang::ASTUnit* ast_unit = nullptr;
// We only consider one full parse on one input file for now, so we only ever
// need one Action.
auto action = llvm::make_unique<Action>(*this);
do {
BeginNextIteration();
if (!ast_unit) {
ast_unit = clang::ASTUnit::LoadFromCompilerInvocationAction(
invocation, pch_container_ops, diags, action.get(), ast_unit,
/*Persistent*/ false, llvm::StringRef(),
/*OnlyLocalDecls*/ false,
/*CaptureDiagnostics*/ true,
/*PrecompilePreamble*/ true,
/*CacheCodeCompletionResults*/ false,
/*IncludeBriefCommentsInCodeCompletion*/ false,
/*UserFilesAreVolatile*/ true,
/*ErrAST*/ nullptr);
// The preprocessor hooks must have configured the FileTracker.
if (action->tracker() == nullptr) {
absl::FPrintF(stderr, "Error: Never entered input file.\n");
return false;
}
} else {
// ASTUnit::Reparse does the following:
// PreprocessorOptions &PPOpts = Invocation->getPreprocessorOpts();
// for (const auto &RB : PPOpts.RemappedFileBuffers)
// delete RB.second;
// It then adds back the buffers that were passed to Reparse.
// Since we don't want our buffers to be deleted, we have to clear out
// the ones ASTUnit might touch, then pass it a new list.
invocation->getPreprocessorOpts().RemappedFileBuffers.clear();
std::vector<std::pair<std::string, llvm::MemoryBuffer*>> buffers;
RemapFiles(invocation->getHeaderSearchOpts().ResourceDir, &buffers);
// Reparse doesn't offer any way to run actions, so we're limited here
// to checking whether our edits were successful (or perhaps to
// driving new edits only from stored diagnostics). If we need to
// start from scratch, we'll have to create a new ASTUnit or re-run the
// invocation entirely. ActionFactory (and FileTracker) are built the
// way they are to permit them to persist beyond SourceManager/FileID
// churn.
ast_unit->Reparse(pch_container_ops, buffers);
clang::SourceLocation old_begin = action->tracker()->file_begin();
clang::FileID old_id = ast_unit->getSourceManager().getFileID(old_begin);
action->tracker()->BeginPass();
// Restore the file begin marker, since we won't get any preprocessor
// events during Reparse. (We can restore other markers if we'd like
// by computing offsets to this marker.)
action->tracker()->set_file_begin(
ast_unit->getSourceManager().getLocForStartOfFile(old_id));
}
// Decide whether we can do anything about the diagnostics.
for (auto d = ast_unit->stored_diag_afterDriver_begin(),
e = ast_unit->stored_diag_end();
d != e; ++d) {
action->tracker()->HandleStoredDiagnostic(*d);
}
clang::Rewriter rewriter(ast_unit->getSourceManager(),
ast_unit->getLangOpts());
if (action->tracker()->Rewrite(&rewriter)) {
// There are actions we should take.
action->tracker()->CommitRewrite(ast_unit->getSourceManager().getFileID(
action->tracker()->file_begin()),
&rewriter);
} else if (iterations_ == 0) {
action->tracker()->mark_failed();
}
} while (action->tracker()->state() == FileTracker::State::kBusy &&
ShouldRunAgain());
if (action->tracker()->state() != FileTracker::State::kFailure) {
const auto buffer = action->tracker()->backing_store();
if (!buffer.empty()) {
absl::PrintF("%s", buffer.str());
}
}
return action->tracker()->state() == FileTracker::State::kSuccess;
}
} // namespace fyi
} // namespace kythe