1 //===--- Check.cpp - clangd self-diagnostics ------------------------------===// 2 // 3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 4 // See https://llvm.org/LICENSE.txt for license information. 5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 6 // 7 //===----------------------------------------------------------------------===// 8 // 9 // Many basic problems can occur processing a file in clangd, e.g.: 10 // - system includes are not found 11 // - crash when indexing its AST 12 // clangd --check provides a simplified, isolated way to reproduce these, 13 // with no editor, LSP, threads, background indexing etc to contend with. 14 // 15 // One important use case is gathering information for bug reports. 16 // Another is reproducing crashes, and checking which setting prevent them. 17 // 18 // It simulates opening a file (determining compile command, parsing, indexing) 19 // and then running features at many locations. 20 // 21 // Currently it adds some basic logging of progress and results. 22 // We should consider extending it to also recognize common symptoms and 23 // recommend solutions (e.g. standard library installation issues). 24 // 25 //===----------------------------------------------------------------------===// 26 27 #include "ClangdLSPServer.h" 28 #include "CodeComplete.h" 29 #include "Config.h" 30 #include "GlobalCompilationDatabase.h" 31 #include "Hover.h" 32 #include "ParsedAST.h" 33 #include "Preamble.h" 34 #include "SourceCode.h" 35 #include "XRefs.h" 36 #include "index/CanonicalIncludes.h" 37 #include "index/FileIndex.h" 38 #include "refactor/Tweak.h" 39 #include "support/ThreadsafeFS.h" 40 #include "clang/AST/ASTContext.h" 41 #include "clang/Basic/DiagnosticIDs.h" 42 #include "clang/Format/Format.h" 43 #include "clang/Frontend/CompilerInvocation.h" 44 #include "clang/Tooling/CompilationDatabase.h" 45 #include "llvm/ADT/ArrayRef.h" 46 #include "llvm/ADT/Optional.h" 47 #include "llvm/ADT/StringExtras.h" 48 #include "llvm/Support/Path.h" 49 50 namespace clang { 51 namespace clangd { 52 namespace { 53 54 // Print (and count) the error-level diagnostics (warnings are ignored). 55 unsigned showErrors(llvm::ArrayRef<Diag> Diags) { 56 unsigned ErrCount = 0; 57 for (const auto &D : Diags) { 58 if (D.Severity >= DiagnosticsEngine::Error) { 59 elog("[{0}] Line {1}: {2}", D.Name, D.Range.start.line + 1, D.Message); 60 ++ErrCount; 61 } 62 } 63 return ErrCount; 64 } 65 66 // This class is just a linear pipeline whose functions get called in sequence. 67 // Each exercises part of clangd's logic on our test file and logs results. 68 // Later steps depend on state built in earlier ones (such as the AST). 69 // Many steps can fatally fail (return false), then subsequent ones cannot run. 70 // Nonfatal failures are logged and tracked in ErrCount. 71 class Checker { 72 // from constructor 73 std::string File; 74 ClangdLSPServer::Options Opts; 75 // from buildCommand 76 tooling::CompileCommand Cmd; 77 // from buildInvocation 78 ParseInputs Inputs; 79 std::unique_ptr<CompilerInvocation> Invocation; 80 format::FormatStyle Style; 81 // from buildAST 82 std::shared_ptr<const PreambleData> Preamble; 83 llvm::Optional<ParsedAST> AST; 84 FileIndex Index; 85 86 public: 87 // Number of non-fatal errors seen. 88 unsigned ErrCount = 0; 89 90 Checker(llvm::StringRef File, const ClangdLSPServer::Options &Opts) 91 : File(File), Opts(Opts) {} 92 93 // Read compilation database and choose a compile command for the file. 94 bool buildCommand(const ThreadsafeFS &TFS) { 95 log("Loading compilation database..."); 96 DirectoryBasedGlobalCompilationDatabase::Options CDBOpts(TFS); 97 CDBOpts.CompileCommandsDir = 98 Config::current().CompileFlags.CDBSearch.FixedCDBPath; 99 std::unique_ptr<GlobalCompilationDatabase> BaseCDB = 100 std::make_unique<DirectoryBasedGlobalCompilationDatabase>(CDBOpts); 101 BaseCDB = getQueryDriverDatabase(llvm::makeArrayRef(Opts.QueryDriverGlobs), 102 std::move(BaseCDB)); 103 auto Mangler = CommandMangler::detect(); 104 if (Opts.ResourceDir) 105 Mangler.ResourceDir = *Opts.ResourceDir; 106 auto CDB = std::make_unique<OverlayCDB>( 107 BaseCDB.get(), std::vector<std::string>{}, 108 tooling::ArgumentsAdjuster(std::move(Mangler))); 109 110 if (auto TrueCmd = CDB->getCompileCommand(File)) { 111 Cmd = std::move(*TrueCmd); 112 log("Compile command from CDB is: {0}", printArgv(Cmd.CommandLine)); 113 } else { 114 Cmd = CDB->getFallbackCommand(File); 115 log("Generic fallback command is: {0}", printArgv(Cmd.CommandLine)); 116 } 117 118 return true; 119 } 120 121 // Prepare inputs and build CompilerInvocation (parsed compile command). 122 bool buildInvocation(const ThreadsafeFS &TFS, 123 llvm::Optional<std::string> Contents) { 124 StoreDiags CaptureInvocationDiags; 125 std::vector<std::string> CC1Args; 126 Inputs.CompileCommand = Cmd; 127 Inputs.TFS = &TFS; 128 Inputs.ClangTidyProvider = Opts.ClangTidyProvider; 129 if (Contents.hasValue()) { 130 Inputs.Contents = *Contents; 131 log("Imaginary source file contents:\n{0}", Inputs.Contents); 132 } else { 133 if (auto Contents = TFS.view(llvm::None)->getBufferForFile(File)) { 134 Inputs.Contents = Contents->get()->getBuffer().str(); 135 } else { 136 elog("Couldn't read {0}: {1}", File, Contents.getError().message()); 137 return false; 138 } 139 } 140 log("Parsing command..."); 141 Invocation = 142 buildCompilerInvocation(Inputs, CaptureInvocationDiags, &CC1Args); 143 auto InvocationDiags = CaptureInvocationDiags.take(); 144 ErrCount += showErrors(InvocationDiags); 145 log("internal (cc1) args are: {0}", printArgv(CC1Args)); 146 if (!Invocation) { 147 elog("Failed to parse command line"); 148 return false; 149 } 150 151 // FIXME: Check that resource-dir/built-in-headers exist? 152 153 Style = getFormatStyleForFile(File, Inputs.Contents, TFS); 154 155 return true; 156 } 157 158 // Build preamble and AST, and index them. 159 bool buildAST() { 160 log("Building preamble..."); 161 Preamble = 162 buildPreamble(File, *Invocation, Inputs, /*StoreInMemory=*/true, 163 [&](ASTContext &Ctx, std::shared_ptr<Preprocessor> PP, 164 const CanonicalIncludes &Includes) { 165 if (!Opts.BuildDynamicSymbolIndex) 166 return; 167 log("Indexing headers..."); 168 Index.updatePreamble(File, /*Version=*/"null", Ctx, 169 std::move(PP), Includes); 170 }); 171 if (!Preamble) { 172 elog("Failed to build preamble"); 173 return false; 174 } 175 ErrCount += showErrors(Preamble->Diags); 176 177 log("Building AST..."); 178 AST = ParsedAST::build(File, Inputs, std::move(Invocation), 179 /*InvocationDiags=*/std::vector<Diag>{}, Preamble); 180 if (!AST) { 181 elog("Failed to build AST"); 182 return false; 183 } 184 ErrCount += showErrors(llvm::makeArrayRef(*AST->getDiagnostics()) 185 .drop_front(Preamble->Diags.size())); 186 187 if (Opts.BuildDynamicSymbolIndex) { 188 log("Indexing AST..."); 189 Index.updateMain(File, *AST); 190 } 191 return true; 192 } 193 194 // Run AST-based features at each token in the file. 195 void testLocationFeatures( 196 llvm::function_ref<bool(const Position &)> ShouldCheckLine) { 197 log("Testing features at each token (may be slow in large files)"); 198 auto &SM = AST->getSourceManager(); 199 auto SpelledTokens = AST->getTokens().spelledTokens(SM.getMainFileID()); 200 for (const auto &Tok : SpelledTokens) { 201 unsigned Start = AST->getSourceManager().getFileOffset(Tok.location()); 202 unsigned End = Start + Tok.length(); 203 Position Pos = offsetToPosition(Inputs.Contents, Start); 204 205 if (!ShouldCheckLine(Pos)) 206 continue; 207 208 // FIXME: dumping the tokens may leak sensitive code into bug reports. 209 // Add an option to turn this off, once we decide how options work. 210 vlog(" {0} {1}", Pos, Tok.text(AST->getSourceManager())); 211 auto Tree = SelectionTree::createRight(AST->getASTContext(), 212 AST->getTokens(), Start, End); 213 Tweak::Selection Selection(&Index, *AST, Start, End, std::move(Tree), 214 nullptr); 215 for (const auto &T : 216 prepareTweaks(Selection, Opts.TweakFilter, Opts.FeatureModules)) { 217 auto Result = T->apply(Selection); 218 if (!Result) { 219 elog(" tweak: {0} ==> FAIL: {1}", T->id(), Result.takeError()); 220 ++ErrCount; 221 } else { 222 vlog(" tweak: {0}", T->id()); 223 } 224 } 225 unsigned Definitions = locateSymbolAt(*AST, Pos, &Index).size(); 226 vlog(" definition: {0}", Definitions); 227 228 auto Hover = getHover(*AST, Pos, Style, &Index); 229 vlog(" hover: {0}", Hover.hasValue()); 230 231 // FIXME: it'd be nice to include code completion, but it's too slow. 232 // Maybe in combination with a line restriction? 233 } 234 } 235 }; 236 237 } // namespace 238 239 bool check(llvm::StringRef File, 240 llvm::function_ref<bool(const Position &)> ShouldCheckLine, 241 const ThreadsafeFS &TFS, const ClangdLSPServer::Options &Opts) { 242 llvm::SmallString<0> FakeFile; 243 llvm::Optional<std::string> Contents; 244 if (File.empty()) { 245 llvm::sys::path::system_temp_directory(false, FakeFile); 246 llvm::sys::path::append(FakeFile, "test.cc"); 247 File = FakeFile; 248 Contents = R"cpp( 249 #include <stddef.h> 250 #include <string> 251 252 size_t N = 50; 253 auto xxx = std::string(N, 'x'); 254 )cpp"; 255 } 256 log("Testing on source file {0}", File); 257 258 auto ContextProvider = ClangdServer::createConfiguredContextProvider( 259 Opts.ConfigProvider, nullptr); 260 WithContext Ctx(ContextProvider("")); 261 Checker C(File, Opts); 262 if (!C.buildCommand(TFS) || !C.buildInvocation(TFS, Contents) || 263 !C.buildAST()) 264 return false; 265 C.testLocationFeatures(ShouldCheckLine); 266 267 log("All checks completed, {0} errors", C.ErrCount); 268 return C.ErrCount == 0; 269 } 270 271 } // namespace clangd 272 } // namespace clang 273