//===--- TestAST.cpp ------------------------------------------------------===// // // 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 "clang/Testing/TestAST.h" #include "clang/Basic/Diagnostic.h" #include "clang/Basic/LangOptions.h" #include "clang/Frontend/FrontendActions.h" #include "clang/Frontend/TextDiagnostic.h" #include "clang/Testing/CommandLineArgs.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/Support/VirtualFileSystem.h" #include "gtest/gtest.h" #include namespace clang { namespace { // Captures diagnostics into a vector, optionally reporting errors to gtest. class StoreDiagnostics : public DiagnosticConsumer { std::vector &Out; bool ReportErrors; LangOptions LangOpts; public: StoreDiagnostics(std::vector &Out, bool ReportErrors) : Out(Out), ReportErrors(ReportErrors) {} void BeginSourceFile(const LangOptions &LangOpts, const Preprocessor *) override { this->LangOpts = LangOpts; } void HandleDiagnostic(DiagnosticsEngine::Level DiagLevel, const Diagnostic &Info) override { Out.emplace_back(DiagLevel, Info); if (ReportErrors && DiagLevel >= DiagnosticsEngine::Error) { std::string Text; llvm::raw_string_ostream OS(Text); TextDiagnostic Renderer(OS, LangOpts, &Info.getDiags()->getDiagnosticOptions()); Renderer.emitStoredDiagnostic(Out.back()); ADD_FAILURE() << Text; } } }; // Fills in the bits of a CompilerInstance that weren't initialized yet. // Provides "empty" ASTContext etc if we fail before parsing gets started. void createMissingComponents(CompilerInstance &Clang) { if (!Clang.hasDiagnostics()) Clang.createDiagnostics(); if (!Clang.hasFileManager()) Clang.createFileManager(); if (!Clang.hasSourceManager()) Clang.createSourceManager(Clang.getFileManager()); if (!Clang.hasTarget()) Clang.createTarget(); if (!Clang.hasPreprocessor()) Clang.createPreprocessor(TU_Complete); if (!Clang.hasASTConsumer()) Clang.setASTConsumer(std::make_unique()); if (!Clang.hasASTContext()) Clang.createASTContext(); if (!Clang.hasSema()) Clang.createSema(TU_Complete, /*CodeCompleteConsumer=*/nullptr); } } // namespace TestAST::TestAST(const TestInputs &In) { Clang = std::make_unique( std::make_shared()); // If we don't manage to finish parsing, create CompilerInstance components // anyway so that the test will see an empty AST instead of crashing. auto RecoverFromEarlyExit = llvm::make_scope_exit([&] { createMissingComponents(*Clang); }); // Extra error conditions are reported through diagnostics, set that up first. bool ErrorOK = In.ErrorOK || llvm::StringRef(In.Code).contains("error-ok"); Clang->createDiagnostics(new StoreDiagnostics(Diagnostics, !ErrorOK)); // Parse cc1 argv, (typically [-std=c++20 input.cc]) into CompilerInvocation. std::vector Argv; std::vector LangArgs = getCC1ArgsForTesting(In.Language); for (const auto &S : LangArgs) Argv.push_back(S.c_str()); for (const auto &S : In.ExtraArgs) Argv.push_back(S.c_str()); std::string Filename = In.FileName; if (Filename.empty()) Filename = getFilenameForTesting(In.Language).str(); Argv.push_back(Filename.c_str()); Clang->setInvocation(std::make_unique()); if (!CompilerInvocation::CreateFromArgs(Clang->getInvocation(), Argv, Clang->getDiagnostics(), "clang")) { ADD_FAILURE() << "Failed to create invocation"; return; } assert(!Clang->getInvocation().getFrontendOpts().DisableFree); // Set up a VFS with only the virtual file visible. auto VFS = llvm::makeIntrusiveRefCnt(); VFS->addFile(Filename, /*ModificationTime=*/0, llvm::MemoryBuffer::getMemBufferCopy(In.Code, Filename)); for (const auto &Extra : In.ExtraFiles) VFS->addFile( Extra.getKey(), /*ModificationTime=*/0, llvm::MemoryBuffer::getMemBufferCopy(Extra.getValue(), Extra.getKey())); Clang->createFileManager(VFS); // Running the FrontendAction creates the other components: SourceManager, // Preprocessor, ASTContext, Sema. Preprocessor needs TargetInfo to be set. EXPECT_TRUE(Clang->createTarget()); Action = In.MakeAction ? In.MakeAction() : std::make_unique(); const FrontendInputFile &Main = Clang->getFrontendOpts().Inputs.front(); if (!Action->BeginSourceFile(*Clang, Main)) { ADD_FAILURE() << "Failed to BeginSourceFile()"; Action.reset(); // Don't call EndSourceFile if BeginSourceFile failed. return; } if (auto Err = Action->Execute()) ADD_FAILURE() << "Failed to Execute(): " << llvm::toString(std::move(Err)); // Action->EndSourceFile() would destroy the ASTContext, we want to keep it. // But notify the preprocessor we're done now. Clang->getPreprocessor().EndSourceFile(); // We're done gathering diagnostics, detach the consumer so we can destroy it. Clang->getDiagnosticClient().EndSourceFile(); Clang->getDiagnostics().setClient(new DiagnosticConsumer(), /*ShouldOwnClient=*/true); } void TestAST::clear() { if (Action) { // We notified the preprocessor of EOF already, so detach it first. // Sema needs the PP alive until after EndSourceFile() though. auto PP = Clang->getPreprocessorPtr(); // Keep PP alive for now. Clang->setPreprocessor(nullptr); // Detach so we don't send EOF twice. Action->EndSourceFile(); // Destroy ASTContext and Sema. // Now Sema is gone, PP can safely be destroyed. } Action.reset(); Clang.reset(); Diagnostics.clear(); } TestAST &TestAST::operator=(TestAST &&M) { clear(); Action = std::move(M.Action); Clang = std::move(M.Clang); Diagnostics = std::move(M.Diagnostics); return *this; } TestAST::TestAST(TestAST &&M) { *this = std::move(M); } TestAST::~TestAST() { clear(); } } // end namespace clang