| //===--- TestTU.cpp - Scratch source files for testing --------------------===// |
| // |
| // 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 "TestTU.h" |
| #include "CompileCommands.h" |
| #include "Compiler.h" |
| #include "Diagnostics.h" |
| #include "TestFS.h" |
| #include "index/FileIndex.h" |
| #include "index/MemIndex.h" |
| #include "clang/AST/RecursiveASTVisitor.h" |
| #include "clang/Basic/Diagnostic.h" |
| #include "clang/Frontend/CompilerInvocation.h" |
| #include "clang/Frontend/Utils.h" |
| #include "llvm/ADT/ScopeExit.h" |
| #include "llvm/Support/ScopedPrinter.h" |
| #include "llvm/Support/raw_ostream.h" |
| #include <cstdlib> |
| |
| namespace clang { |
| namespace clangd { |
| |
| ParseInputs TestTU::inputs(MockFS &FS) const { |
| std::string FullFilename = testPath(Filename), |
| FullHeaderName = testPath(HeaderFilename), |
| ImportThunk = testPath("import_thunk.h"); |
| // We want to implicitly include HeaderFilename without messing up offsets. |
| // -include achieves this, but sometimes we want #import (to simulate a header |
| // guard without messing up offsets). In this case, use an intermediate file. |
| std::string ThunkContents = "#import \"" + FullHeaderName + "\"\n"; |
| |
| FS.Files = AdditionalFiles; |
| FS.Files[FullFilename] = Code; |
| FS.Files[FullHeaderName] = HeaderCode; |
| FS.Files[ImportThunk] = ThunkContents; |
| |
| ParseInputs Inputs; |
| Inputs.FeatureModules = FeatureModules; |
| auto &Argv = Inputs.CompileCommand.CommandLine; |
| Argv = {"clang"}; |
| // FIXME: this shouldn't need to be conditional, but it breaks a |
| // GoToDefinition test for some reason (getMacroArgExpandedLocation fails). |
| if (!HeaderCode.empty()) { |
| Argv.push_back("-include"); |
| Argv.push_back(ImplicitHeaderGuard ? ImportThunk : FullHeaderName); |
| // ms-compatibility changes the meaning of #import. |
| // The default is OS-dependent (on on windows), ensure it's off. |
| if (ImplicitHeaderGuard) |
| Inputs.CompileCommand.CommandLine.push_back("-fno-ms-compatibility"); |
| } |
| Argv.insert(Argv.end(), ExtraArgs.begin(), ExtraArgs.end()); |
| // Put the file name at the end -- this allows the extra arg (-xc++) to |
| // override the language setting. |
| Argv.push_back(FullFilename); |
| |
| auto Mangler = CommandMangler::forTests(); |
| Mangler.adjust(Inputs.CompileCommand.CommandLine, FullFilename); |
| Inputs.CompileCommand.Filename = FullFilename; |
| Inputs.CompileCommand.Directory = testRoot(); |
| Inputs.Contents = Code; |
| if (OverlayRealFileSystemForModules) |
| FS.OverlayRealFileSystemForModules = true; |
| Inputs.TFS = &FS; |
| Inputs.Opts = ParseOptions(); |
| if (ClangTidyProvider) |
| Inputs.ClangTidyProvider = ClangTidyProvider; |
| Inputs.Index = ExternalIndex; |
| return Inputs; |
| } |
| |
| void initializeModuleCache(CompilerInvocation &CI) { |
| llvm::SmallString<128> ModuleCachePath; |
| if (llvm::sys::fs::createUniqueDirectory("module-cache", ModuleCachePath)) { |
| llvm::errs() << "Failed to create temp directory for module-cache"; |
| std::abort(); |
| } |
| CI.getHeaderSearchOpts().ModuleCachePath = ModuleCachePath.c_str(); |
| } |
| |
| void deleteModuleCache(const std::string ModuleCachePath) { |
| if (!ModuleCachePath.empty()) { |
| if (llvm::sys::fs::remove_directories(ModuleCachePath)) { |
| llvm::errs() << "Failed to delete temp directory for module-cache"; |
| std::abort(); |
| } |
| } |
| } |
| |
| std::shared_ptr<const PreambleData> |
| TestTU::preamble(PreambleParsedCallback PreambleCallback) const { |
| MockFS FS; |
| auto Inputs = inputs(FS); |
| IgnoreDiagnostics Diags; |
| auto CI = buildCompilerInvocation(Inputs, Diags); |
| assert(CI && "Failed to build compilation invocation."); |
| if (OverlayRealFileSystemForModules) |
| initializeModuleCache(*CI); |
| auto ModuleCacheDeleter = llvm::make_scope_exit( |
| std::bind(deleteModuleCache, CI->getHeaderSearchOpts().ModuleCachePath)); |
| return clang::clangd::buildPreamble(testPath(Filename), *CI, Inputs, |
| /*StoreInMemory=*/true, PreambleCallback); |
| } |
| |
| ParsedAST TestTU::build() const { |
| MockFS FS; |
| auto Inputs = inputs(FS); |
| StoreDiags Diags; |
| auto CI = buildCompilerInvocation(Inputs, Diags); |
| assert(CI && "Failed to build compilation invocation."); |
| if (OverlayRealFileSystemForModules) |
| initializeModuleCache(*CI); |
| auto ModuleCacheDeleter = llvm::make_scope_exit( |
| std::bind(deleteModuleCache, CI->getHeaderSearchOpts().ModuleCachePath)); |
| |
| auto Preamble = clang::clangd::buildPreamble(testPath(Filename), *CI, Inputs, |
| /*StoreInMemory=*/true, |
| /*PreambleCallback=*/nullptr); |
| auto AST = ParsedAST::build(testPath(Filename), Inputs, std::move(CI), |
| Diags.take(), Preamble); |
| if (!AST.hasValue()) { |
| llvm::errs() << "Failed to build code:\n" << Code; |
| std::abort(); |
| } |
| assert(AST->getDiagnostics() && |
| "TestTU should always build an AST with a fresh Preamble"); |
| // Check for error diagnostics and report gtest failures (unless expected). |
| // This guards against accidental syntax errors silently subverting tests. |
| // error-ok is awfully primitive - using clang -verify would be nicer. |
| // Ownership and layering makes it pretty hard. |
| bool ErrorOk = [&, this] { |
| llvm::StringLiteral Marker = "error-ok"; |
| if (llvm::StringRef(Code).contains(Marker) || |
| llvm::StringRef(HeaderCode).contains(Marker)) |
| return true; |
| for (const auto &KV : this->AdditionalFiles) |
| if (llvm::StringRef(KV.second).contains(Marker)) |
| return true; |
| return false; |
| }(); |
| if (!ErrorOk) { |
| // We always build AST with a fresh preamble in TestTU. |
| for (const auto &D : *AST->getDiagnostics()) |
| if (D.Severity >= DiagnosticsEngine::Error) { |
| llvm::errs() |
| << "TestTU failed to build (suppress with /*error-ok*/): \n" |
| << D << "\n\nFor code:\n" |
| << Code; |
| std::abort(); // Stop after first error for simplicity. |
| } |
| } |
| return std::move(*AST); |
| } |
| |
| SymbolSlab TestTU::headerSymbols() const { |
| auto AST = build(); |
| return std::get<0>(indexHeaderSymbols(/*Version=*/"null", AST.getASTContext(), |
| AST.getPreprocessorPtr(), |
| AST.getCanonicalIncludes())); |
| } |
| |
| RefSlab TestTU::headerRefs() const { |
| auto AST = build(); |
| return std::get<1>(indexMainDecls(AST)); |
| } |
| |
| std::unique_ptr<SymbolIndex> TestTU::index() const { |
| auto AST = build(); |
| auto Idx = std::make_unique<FileIndex>(); |
| Idx->updatePreamble(testPath(Filename), /*Version=*/"null", |
| AST.getASTContext(), AST.getPreprocessorPtr(), |
| AST.getCanonicalIncludes()); |
| Idx->updateMain(testPath(Filename), AST); |
| return std::move(Idx); |
| } |
| |
| const Symbol &findSymbol(const SymbolSlab &Slab, llvm::StringRef QName) { |
| const Symbol *Result = nullptr; |
| for (const Symbol &S : Slab) { |
| if (QName != (S.Scope + S.Name).str()) |
| continue; |
| if (Result) { |
| llvm::errs() << "Multiple symbols named " << QName << ":\n" |
| << *Result << "\n---\n" |
| << S; |
| assert(false && "QName is not unique"); |
| } |
| Result = &S; |
| } |
| if (!Result) { |
| llvm::errs() << "No symbol named " << QName << " in " |
| << llvm::to_string(Slab); |
| assert(false && "No symbol with QName"); |
| } |
| return *Result; |
| } |
| |
| // RAII scoped class to disable TraversalScope for a ParsedAST. |
| class TraverseHeadersToo { |
| ASTContext &Ctx; |
| std::vector<Decl *> ScopeToRestore; |
| |
| public: |
| TraverseHeadersToo(ParsedAST &AST) |
| : Ctx(AST.getASTContext()), ScopeToRestore(Ctx.getTraversalScope()) { |
| Ctx.setTraversalScope({Ctx.getTranslationUnitDecl()}); |
| } |
| ~TraverseHeadersToo() { Ctx.setTraversalScope(std::move(ScopeToRestore)); } |
| }; |
| |
| const NamedDecl &findDecl(ParsedAST &AST, llvm::StringRef QName) { |
| auto &Ctx = AST.getASTContext(); |
| auto LookupDecl = [&Ctx](const DeclContext &Scope, |
| llvm::StringRef Name) -> const NamedDecl & { |
| auto LookupRes = Scope.lookup(DeclarationName(&Ctx.Idents.get(Name))); |
| assert(!LookupRes.empty() && "Lookup failed"); |
| assert(LookupRes.isSingleResult() && "Lookup returned multiple results"); |
| return *LookupRes.front(); |
| }; |
| |
| const DeclContext *Scope = Ctx.getTranslationUnitDecl(); |
| |
| StringRef Cur, Rest; |
| for (std::tie(Cur, Rest) = QName.split("::"); !Rest.empty(); |
| std::tie(Cur, Rest) = Rest.split("::")) { |
| Scope = &cast<DeclContext>(LookupDecl(*Scope, Cur)); |
| } |
| return LookupDecl(*Scope, Cur); |
| } |
| |
| const NamedDecl &findDecl(ParsedAST &AST, |
| std::function<bool(const NamedDecl &)> Filter) { |
| TraverseHeadersToo Too(AST); |
| struct Visitor : RecursiveASTVisitor<Visitor> { |
| decltype(Filter) F; |
| llvm::SmallVector<const NamedDecl *, 1> Decls; |
| bool VisitNamedDecl(const NamedDecl *ND) { |
| if (F(*ND)) |
| Decls.push_back(ND); |
| return true; |
| } |
| } Visitor; |
| Visitor.F = Filter; |
| Visitor.TraverseDecl(AST.getASTContext().getTranslationUnitDecl()); |
| if (Visitor.Decls.size() != 1) { |
| llvm::errs() << Visitor.Decls.size() << " symbols matched."; |
| assert(Visitor.Decls.size() == 1); |
| } |
| return *Visitor.Decls.front(); |
| } |
| |
| const NamedDecl &findUnqualifiedDecl(ParsedAST &AST, llvm::StringRef Name) { |
| return findDecl(AST, [Name](const NamedDecl &ND) { |
| if (auto *ID = ND.getIdentifier()) |
| if (ID->getName() == Name) |
| return true; |
| return false; |
| }); |
| } |
| |
| } // namespace clangd |
| } // namespace clang |