llvm-project/clang-tools-extra/clang-include-fixer/IncludeFixer.cpp

446 lines
17 KiB
C++

//===-- IncludeFixer.cpp - Include inserter based on sema callbacks -------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "IncludeFixer.h"
#include "clang/Format/Format.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Lex/HeaderSearch.h"
#include "clang/Lex/Preprocessor.h"
#include "clang/Parse/ParseAST.h"
#include "clang/Sema/Sema.h"
#include "llvm/Support/Debug.h"
#include "llvm/Support/raw_ostream.h"
#define DEBUG_TYPE "clang-include-fixer"
using namespace clang;
namespace clang {
namespace include_fixer {
namespace {
/// Manages the parse, gathers include suggestions.
class Action : public clang::ASTFrontendAction {
public:
explicit Action(SymbolIndexManager &SymbolIndexMgr, bool MinimizeIncludePaths)
: SemaSource(SymbolIndexMgr, MinimizeIncludePaths,
/*GenerateDiagnostics=*/false) {}
std::unique_ptr<clang::ASTConsumer>
CreateASTConsumer(clang::CompilerInstance &Compiler,
StringRef InFile) override {
SemaSource.setFilePath(InFile);
return std::make_unique<clang::ASTConsumer>();
}
void ExecuteAction() override {
clang::CompilerInstance *Compiler = &getCompilerInstance();
assert(!Compiler->hasSema() && "CI already has Sema");
// Set up our hooks into sema and parse the AST.
if (hasCodeCompletionSupport() &&
!Compiler->getFrontendOpts().CodeCompletionAt.FileName.empty())
Compiler->createCodeCompletionConsumer();
clang::CodeCompleteConsumer *CompletionConsumer = nullptr;
if (Compiler->hasCodeCompletionConsumer())
CompletionConsumer = &Compiler->getCodeCompletionConsumer();
Compiler->createSema(getTranslationUnitKind(), CompletionConsumer);
SemaSource.setCompilerInstance(Compiler);
Compiler->getSema().addExternalSource(&SemaSource);
clang::ParseAST(Compiler->getSema(), Compiler->getFrontendOpts().ShowStats,
Compiler->getFrontendOpts().SkipFunctionBodies);
}
IncludeFixerContext
getIncludeFixerContext(const clang::SourceManager &SourceManager,
clang::HeaderSearch &HeaderSearch) const {
return SemaSource.getIncludeFixerContext(SourceManager, HeaderSearch,
SemaSource.getMatchedSymbols());
}
private:
IncludeFixerSemaSource SemaSource;
};
} // namespace
IncludeFixerActionFactory::IncludeFixerActionFactory(
SymbolIndexManager &SymbolIndexMgr,
std::vector<IncludeFixerContext> &Contexts, StringRef StyleName,
bool MinimizeIncludePaths)
: SymbolIndexMgr(SymbolIndexMgr), Contexts(Contexts),
MinimizeIncludePaths(MinimizeIncludePaths) {}
IncludeFixerActionFactory::~IncludeFixerActionFactory() = default;
bool IncludeFixerActionFactory::runInvocation(
std::shared_ptr<clang::CompilerInvocation> Invocation,
clang::FileManager *Files,
std::shared_ptr<clang::PCHContainerOperations> PCHContainerOps,
clang::DiagnosticConsumer *Diagnostics) {
assert(Invocation->getFrontendOpts().Inputs.size() == 1);
// Set up Clang.
clang::CompilerInstance Compiler(PCHContainerOps);
Compiler.setInvocation(std::move(Invocation));
Compiler.setFileManager(Files);
// Create the compiler's actual diagnostics engine. We want to drop all
// diagnostics here.
Compiler.createDiagnostics(new clang::IgnoringDiagConsumer,
/*ShouldOwnClient=*/true);
Compiler.createSourceManager(*Files);
// We abort on fatal errors so don't let a large number of errors become
// fatal. A missing #include can cause thousands of errors.
Compiler.getDiagnostics().setErrorLimit(0);
// Run the parser, gather missing includes.
auto ScopedToolAction =
std::make_unique<Action>(SymbolIndexMgr, MinimizeIncludePaths);
Compiler.ExecuteAction(*ScopedToolAction);
Contexts.push_back(ScopedToolAction->getIncludeFixerContext(
Compiler.getSourceManager(),
Compiler.getPreprocessor().getHeaderSearchInfo()));
// Technically this should only return true if we're sure that we have a
// parseable file. We don't know that though. Only inform users of fatal
// errors.
return !Compiler.getDiagnostics().hasFatalErrorOccurred();
}
static bool addDiagnosticsForContext(TypoCorrection &Correction,
const IncludeFixerContext &Context,
StringRef Code, SourceLocation StartOfFile,
ASTContext &Ctx) {
auto Reps = createIncludeFixerReplacements(
Code, Context, format::getLLVMStyle(), /*AddQualifiers=*/false);
if (!Reps || Reps->size() != 1)
return false;
unsigned DiagID = Ctx.getDiagnostics().getCustomDiagID(
DiagnosticsEngine::Note, "Add '#include %0' to provide the missing "
"declaration [clang-include-fixer]");
// FIXME: Currently we only generate a diagnostic for the first header. Give
// the user choices.
const tooling::Replacement &Placed = *Reps->begin();
auto Begin = StartOfFile.getLocWithOffset(Placed.getOffset());
auto End = Begin.getLocWithOffset(std::max(0, (int)Placed.getLength() - 1));
PartialDiagnostic PD(DiagID, Ctx.getDiagAllocator());
PD << Context.getHeaderInfos().front().Header
<< FixItHint::CreateReplacement(CharSourceRange::getCharRange(Begin, End),
Placed.getReplacementText());
Correction.addExtraDiagnostic(std::move(PD));
return true;
}
/// Callback for incomplete types. If we encounter a forward declaration we
/// have the fully qualified name ready. Just query that.
bool IncludeFixerSemaSource::MaybeDiagnoseMissingCompleteType(
clang::SourceLocation Loc, clang::QualType T) {
// Ignore spurious callbacks from SFINAE contexts.
if (CI->getSema().isSFINAEContext())
return false;
clang::ASTContext &context = CI->getASTContext();
std::string QueryString = QualType(T->getUnqualifiedDesugaredType(), 0)
.getAsString(context.getPrintingPolicy());
LLVM_DEBUG(llvm::dbgs() << "Query missing complete type '" << QueryString
<< "'");
// Pass an empty range here since we don't add qualifier in this case.
std::vector<find_all_symbols::SymbolInfo> MatchedSymbols =
query(QueryString, "", tooling::Range());
if (!MatchedSymbols.empty() && GenerateDiagnostics) {
TypoCorrection Correction;
FileID FID = CI->getSourceManager().getFileID(Loc);
StringRef Code = CI->getSourceManager().getBufferData(FID);
SourceLocation StartOfFile =
CI->getSourceManager().getLocForStartOfFile(FID);
addDiagnosticsForContext(
Correction,
getIncludeFixerContext(CI->getSourceManager(),
CI->getPreprocessor().getHeaderSearchInfo(),
MatchedSymbols),
Code, StartOfFile, CI->getASTContext());
for (const PartialDiagnostic &PD : Correction.getExtraDiagnostics())
CI->getSema().Diag(Loc, PD);
}
return true;
}
/// Callback for unknown identifiers. Try to piece together as much
/// qualification as we can get and do a query.
clang::TypoCorrection IncludeFixerSemaSource::CorrectTypo(
const DeclarationNameInfo &Typo, int LookupKind, Scope *S, CXXScopeSpec *SS,
CorrectionCandidateCallback &CCC, DeclContext *MemberContext,
bool EnteringContext, const ObjCObjectPointerType *OPT) {
// Ignore spurious callbacks from SFINAE contexts.
if (CI->getSema().isSFINAEContext())
return clang::TypoCorrection();
// We currently ignore the unidentified symbol which is not from the
// main file.
//
// However, this is not always true due to templates in a non-self contained
// header, consider the case:
//
// // header.h
// template <typename T>
// class Foo {
// T t;
// };
//
// // test.cc
// // We need to add <bar.h> in test.cc instead of header.h.
// class Bar;
// Foo<Bar> foo;
//
// FIXME: Add the missing header to the header file where the symbol comes
// from.
if (!CI->getSourceManager().isWrittenInMainFile(Typo.getLoc()))
return clang::TypoCorrection();
std::string TypoScopeString;
if (S) {
// FIXME: Currently we only use namespace contexts. Use other context
// types for query.
for (const auto *Context = S->getEntity(); Context;
Context = Context->getParent()) {
if (const auto *ND = dyn_cast<NamespaceDecl>(Context)) {
if (!ND->getName().empty())
TypoScopeString = ND->getNameAsString() + "::" + TypoScopeString;
}
}
}
auto ExtendNestedNameSpecifier = [this](CharSourceRange Range) {
StringRef Source =
Lexer::getSourceText(Range, CI->getSourceManager(), CI->getLangOpts());
// Skip forward until we find a character that's neither identifier nor
// colon. This is a bit of a hack around the fact that we will only get a
// single callback for a long nested name if a part of the beginning is
// unknown. For example:
//
// llvm::sys::path::parent_path(...)
// ^~~~ ^~~
// known
// ^~~~
// unknown, last callback
// ^~~~~~~~~~~
// no callback
//
// With the extension we get the full nested name specifier including
// parent_path.
// FIXME: Don't rely on source text.
const char *End = Source.end();
while (isAsciiIdentifierContinue(*End) || *End == ':')
++End;
return std::string(Source.begin(), End);
};
/// If we have a scope specification, use that to get more precise results.
std::string QueryString;
tooling::Range SymbolRange;
const auto &SM = CI->getSourceManager();
auto CreateToolingRange = [&QueryString, &SM](SourceLocation BeginLoc) {
return tooling::Range(SM.getDecomposedLoc(BeginLoc).second,
QueryString.size());
};
if (SS && SS->getRange().isValid()) {
auto Range = CharSourceRange::getTokenRange(SS->getRange().getBegin(),
Typo.getLoc());
QueryString = ExtendNestedNameSpecifier(Range);
SymbolRange = CreateToolingRange(Range.getBegin());
} else if (Typo.getName().isIdentifier() && !Typo.getLoc().isMacroID()) {
auto Range =
CharSourceRange::getTokenRange(Typo.getBeginLoc(), Typo.getEndLoc());
QueryString = ExtendNestedNameSpecifier(Range);
SymbolRange = CreateToolingRange(Range.getBegin());
} else {
QueryString = Typo.getAsString();
SymbolRange = CreateToolingRange(Typo.getLoc());
}
LLVM_DEBUG(llvm::dbgs() << "TypoScopeQualifiers: " << TypoScopeString
<< "\n");
std::vector<find_all_symbols::SymbolInfo> MatchedSymbols =
query(QueryString, TypoScopeString, SymbolRange);
if (!MatchedSymbols.empty() && GenerateDiagnostics) {
TypoCorrection Correction(Typo.getName());
Correction.setCorrectionRange(SS, Typo);
FileID FID = SM.getFileID(Typo.getLoc());
StringRef Code = SM.getBufferData(FID);
SourceLocation StartOfFile = SM.getLocForStartOfFile(FID);
if (addDiagnosticsForContext(
Correction, getIncludeFixerContext(
SM, CI->getPreprocessor().getHeaderSearchInfo(),
MatchedSymbols),
Code, StartOfFile, CI->getASTContext()))
return Correction;
}
return TypoCorrection();
}
/// Get the minimal include for a given path.
std::string IncludeFixerSemaSource::minimizeInclude(
StringRef Include, const clang::SourceManager &SourceManager,
clang::HeaderSearch &HeaderSearch) const {
if (!MinimizeIncludePaths)
return std::string(Include);
// Get the FileEntry for the include.
StringRef StrippedInclude = Include.trim("\"<>");
auto Entry = SourceManager.getFileManager().getFile(StrippedInclude);
// If the file doesn't exist return the path from the database.
// FIXME: This should never happen.
if (!Entry)
return std::string(Include);
bool IsSystem = false;
std::string Suggestion =
HeaderSearch.suggestPathToFileForDiagnostics(*Entry, "", &IsSystem);
return IsSystem ? '<' + Suggestion + '>' : '"' + Suggestion + '"';
}
/// Get the include fixer context for the queried symbol.
IncludeFixerContext IncludeFixerSemaSource::getIncludeFixerContext(
const clang::SourceManager &SourceManager,
clang::HeaderSearch &HeaderSearch,
ArrayRef<find_all_symbols::SymbolInfo> MatchedSymbols) const {
std::vector<find_all_symbols::SymbolInfo> SymbolCandidates;
for (const auto &Symbol : MatchedSymbols) {
std::string FilePath = Symbol.getFilePath().str();
std::string MinimizedFilePath = minimizeInclude(
((FilePath[0] == '"' || FilePath[0] == '<') ? FilePath
: "\"" + FilePath + "\""),
SourceManager, HeaderSearch);
SymbolCandidates.emplace_back(Symbol.getName(), Symbol.getSymbolKind(),
MinimizedFilePath, Symbol.getContexts());
}
return IncludeFixerContext(FilePath, QuerySymbolInfos, SymbolCandidates);
}
std::vector<find_all_symbols::SymbolInfo>
IncludeFixerSemaSource::query(StringRef Query, StringRef ScopedQualifiers,
tooling::Range Range) {
assert(!Query.empty() && "Empty query!");
// Save all instances of an unidentified symbol.
//
// We use conservative behavior for detecting the same unidentified symbol
// here. The symbols which have the same ScopedQualifier and RawIdentifier
// are considered equal. So that clang-include-fixer avoids false positives,
// and always adds missing qualifiers to correct symbols.
if (!GenerateDiagnostics && !QuerySymbolInfos.empty()) {
if (ScopedQualifiers == QuerySymbolInfos.front().ScopedQualifiers &&
Query == QuerySymbolInfos.front().RawIdentifier) {
QuerySymbolInfos.push_back(
{Query.str(), std::string(ScopedQualifiers), Range});
}
return {};
}
LLVM_DEBUG(llvm::dbgs() << "Looking up '" << Query << "' at ");
LLVM_DEBUG(CI->getSourceManager()
.getLocForStartOfFile(CI->getSourceManager().getMainFileID())
.getLocWithOffset(Range.getOffset())
.print(llvm::dbgs(), CI->getSourceManager()));
LLVM_DEBUG(llvm::dbgs() << " ...");
llvm::StringRef FileName = CI->getSourceManager().getFilename(
CI->getSourceManager().getLocForStartOfFile(
CI->getSourceManager().getMainFileID()));
QuerySymbolInfos.push_back(
{Query.str(), std::string(ScopedQualifiers), Range});
// Query the symbol based on C++ name Lookup rules.
// Firstly, lookup the identifier with scoped namespace contexts;
// If that fails, falls back to look up the identifier directly.
//
// For example:
//
// namespace a {
// b::foo f;
// }
//
// 1. lookup a::b::foo.
// 2. lookup b::foo.
std::string QueryString = ScopedQualifiers.str() + Query.str();
// It's unsafe to do nested search for the identifier with scoped namespace
// context, it might treat the identifier as a nested class of the scoped
// namespace.
std::vector<find_all_symbols::SymbolInfo> MatchedSymbols =
SymbolIndexMgr.search(QueryString, /*IsNestedSearch=*/false, FileName);
if (MatchedSymbols.empty())
MatchedSymbols =
SymbolIndexMgr.search(Query, /*IsNestedSearch=*/true, FileName);
LLVM_DEBUG(llvm::dbgs() << "Having found " << MatchedSymbols.size()
<< " symbols\n");
// We store a copy of MatchedSymbols in a place where it's globally reachable.
// This is used by the standalone version of the tool.
this->MatchedSymbols = MatchedSymbols;
return MatchedSymbols;
}
llvm::Expected<tooling::Replacements> createIncludeFixerReplacements(
StringRef Code, const IncludeFixerContext &Context,
const clang::format::FormatStyle &Style, bool AddQualifiers) {
if (Context.getHeaderInfos().empty())
return tooling::Replacements();
StringRef FilePath = Context.getFilePath();
std::string IncludeName =
"#include " + Context.getHeaderInfos().front().Header + "\n";
// Create replacements for the new header.
clang::tooling::Replacements Insertions;
auto Err =
Insertions.add(tooling::Replacement(FilePath, UINT_MAX, 0, IncludeName));
if (Err)
return std::move(Err);
auto CleanReplaces = cleanupAroundReplacements(Code, Insertions, Style);
if (!CleanReplaces)
return CleanReplaces;
auto Replaces = std::move(*CleanReplaces);
if (AddQualifiers) {
for (const auto &Info : Context.getQuerySymbolInfos()) {
// Ignore the empty range.
if (Info.Range.getLength() > 0) {
auto R = tooling::Replacement(
{FilePath, Info.Range.getOffset(), Info.Range.getLength(),
Context.getHeaderInfos().front().QualifiedName});
auto Err = Replaces.add(R);
if (Err) {
llvm::consumeError(std::move(Err));
R = tooling::Replacement(
R.getFilePath(), Replaces.getShiftedCodePosition(R.getOffset()),
R.getLength(), R.getReplacementText());
Replaces = Replaces.merge(tooling::Replacements(R));
}
}
}
}
return formatReplacements(Code, Replaces, Style);
}
} // namespace include_fixer
} // namespace clang