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 "GlobalCompilationDatabase.h" 30 #include "Hover.h" 31 #include "ParsedAST.h" 32 #include "Preamble.h" 33 #include "SourceCode.h" 34 #include "XRefs.h" 35 #include "index/CanonicalIncludes.h" 36 #include "index/FileIndex.h" 37 #include "refactor/Tweak.h" 38 #include "support/ThreadsafeFS.h" 39 #include "clang/AST/ASTContext.h" 40 #include "clang/Basic/DiagnosticIDs.h" 41 #include "clang/Format/Format.h" 42 #include "clang/Frontend/CompilerInvocation.h" 43 #include "clang/Tooling/CompilationDatabase.h" 44 #include "llvm/ADT/ArrayRef.h" 45 #include "llvm/ADT/Optional.h" 46 #include "llvm/ADT/StringExtras.h" 47 #include "llvm/Support/Path.h" 48 49 namespace clang { 50 namespace clangd { 51 namespace { 52 53 // Print (and count) the error-level diagnostics (warnings are ignored). 54 unsigned showErrors(llvm::ArrayRef<Diag> Diags) { 55 unsigned ErrCount = 0; 56 for (const auto &D : Diags) { 57 if (D.Severity >= DiagnosticsEngine::Error) { 58 elog("[{0}] Line {1}: {2}", D.Name, D.Range.start.line + 1, D.Message); 59 ++ErrCount; 60 } 61 } 62 return ErrCount; 63 } 64 65 // This class is just a linear pipeline whose functions get called in sequence. 66 // Each exercises part of clangd's logic on our test file and logs results. 67 // Later steps depend on state built in earlier ones (such as the AST). 68 // Many steps can fatally fail (return false), then subsequent ones cannot run. 69 // Nonfatal failures are logged and tracked in ErrCount. 70 class Checker { 71 // from constructor 72 std::string File; 73 ClangdLSPServer::Options Opts; 74 // from buildCommand 75 tooling::CompileCommand Cmd; 76 // from buildInvocation 77 ParseInputs Inputs; 78 std::unique_ptr<CompilerInvocation> Invocation; 79 format::FormatStyle Style; 80 // from buildAST 81 std::shared_ptr<const PreambleData> Preamble; 82 llvm::Optional<ParsedAST> AST; 83 FileIndex Index; 84 85 public: 86 // Number of non-fatal errors seen. 87 unsigned ErrCount = 0; 88 89 Checker(llvm::StringRef File, const ClangdLSPServer::Options &Opts) 90 : File(File), Opts(Opts) {} 91 92 // Read compilation database and choose a compile command for the file. 93 bool buildCommand() { 94 log("Loading compilation database..."); 95 std::unique_ptr<GlobalCompilationDatabase> BaseCDB = 96 std::make_unique<DirectoryBasedGlobalCompilationDatabase>( 97 Opts.CompileCommandsDir); 98 BaseCDB = getQueryDriverDatabase(llvm::makeArrayRef(Opts.QueryDriverGlobs), 99 std::move(BaseCDB)); 100 auto Mangler = CommandMangler::detect(); 101 if (Opts.ResourceDir) 102 Mangler.ResourceDir = *Opts.ResourceDir; 103 auto CDB = std::make_unique<OverlayCDB>( 104 BaseCDB.get(), std::vector<std::string>{}, 105 tooling::ArgumentsAdjuster(std::move(Mangler))); 106 107 if (auto TrueCmd = CDB->getCompileCommand(File)) { 108 Cmd = std::move(*TrueCmd); 109 log("Compile command from CDB is: {0}", llvm::join(Cmd.CommandLine, " ")); 110 } else { 111 Cmd = CDB->getFallbackCommand(File); 112 log("Generic fallback command is: {0}", llvm::join(Cmd.CommandLine, " ")); 113 } 114 115 return true; 116 } 117 118 // Prepare inputs and build CompilerInvocation (parsed compile command). 119 bool buildInvocation(const ThreadsafeFS &TFS, 120 llvm::Optional<std::string> Contents) { 121 StoreDiags CaptureInvocationDiags; 122 std::vector<std::string> CC1Args; 123 Inputs.CompileCommand = Cmd; 124 Inputs.TFS = &TFS; 125 if (Contents.hasValue()) { 126 Inputs.Contents = *Contents; 127 log("Imaginary source file contents:\n{0}", Inputs.Contents); 128 } else { 129 if (auto Contents = TFS.view(llvm::None)->getBufferForFile(File)) { 130 Inputs.Contents = Contents->get()->getBuffer().str(); 131 } else { 132 elog("Couldn't read {0}: {1}", File, Contents.getError().message()); 133 return false; 134 } 135 } 136 Inputs.Opts.ClangTidyOpts = 137 Opts.GetClangTidyOptions(*TFS.view(llvm::None), File); 138 log("Parsing command..."); 139 Invocation = 140 buildCompilerInvocation(Inputs, CaptureInvocationDiags, &CC1Args); 141 auto InvocationDiags = CaptureInvocationDiags.take(); 142 ErrCount += showErrors(InvocationDiags); 143 log("internal (cc1) args are: {0}", llvm::join(CC1Args, " ")); 144 if (!Invocation) { 145 elog("Failed to parse command line"); 146 return false; 147 } 148 149 // FIXME: Check that resource-dir/built-in-headers exist? 150 151 Style = getFormatStyleForFile(File, Inputs.Contents, TFS); 152 153 return true; 154 } 155 156 // Build preamble and AST, and index them. 157 bool buildAST() { 158 log("Building preamble..."); 159 Preamble = 160 buildPreamble(File, *Invocation, Inputs, /*StoreInMemory=*/true, 161 [&](ASTContext &Ctx, std::shared_ptr<Preprocessor> PP, 162 const CanonicalIncludes &Includes) { 163 if (!Opts.BuildDynamicSymbolIndex) 164 return; 165 log("Indexing headers..."); 166 Index.updatePreamble(File, /*Version=*/"null", Ctx, 167 std::move(PP), Includes); 168 }); 169 if (!Preamble) { 170 elog("Failed to build preamble"); 171 return false; 172 } 173 ErrCount += showErrors(Preamble->Diags); 174 175 log("Building AST..."); 176 AST = ParsedAST::build(File, Inputs, std::move(Invocation), 177 /*InvocationDiags=*/std::vector<Diag>{}, Preamble); 178 if (!AST) { 179 elog("Failed to build AST"); 180 return false; 181 } 182 ErrCount += showErrors(llvm::makeArrayRef(AST->getDiagnostics()) 183 .drop_front(Preamble->Diags.size())); 184 185 if (Opts.BuildDynamicSymbolIndex) { 186 log("Indexing AST..."); 187 Index.updateMain(File, *AST); 188 } 189 return true; 190 } 191 192 // Run AST-based features at each token in the file. 193 void testLocationFeatures() { 194 log("Testing features at each token (may be slow in large files)"); 195 auto SpelledTokens = 196 AST->getTokens().spelledTokens(AST->getSourceManager().getMainFileID()); 197 for (const auto &Tok : SpelledTokens) { 198 unsigned Start = AST->getSourceManager().getFileOffset(Tok.location()); 199 unsigned End = Start + Tok.length(); 200 Position Pos = offsetToPosition(Inputs.Contents, Start); 201 // FIXME: dumping the tokens may leak sensitive code into bug reports. 202 // Add an option to turn this off, once we decide how options work. 203 vlog(" {0} {1}", Pos, Tok.text(AST->getSourceManager())); 204 auto Tree = SelectionTree::createRight(AST->getASTContext(), 205 AST->getTokens(), Start, End); 206 Tweak::Selection Selection(&Index, *AST, Start, End, std::move(Tree)); 207 for (const auto &T : prepareTweaks(Selection, Opts.TweakFilter)) { 208 auto Result = T->apply(Selection); 209 if (!Result) { 210 elog(" tweak: {0} ==> FAIL: {1}", T->id(), Result.takeError()); 211 ++ErrCount; 212 } else { 213 vlog(" tweak: {0}", T->id()); 214 } 215 } 216 unsigned Definitions = locateSymbolAt(*AST, Pos, &Index).size(); 217 vlog(" definition: {0}", Definitions); 218 219 auto Hover = getHover(*AST, Pos, Style, &Index); 220 vlog(" hover: {0}", Hover.hasValue()); 221 222 // FIXME: it'd be nice to include code completion, but it's too slow. 223 // Maybe in combination with a line restriction? 224 } 225 } 226 }; 227 228 } // namespace 229 230 bool check(llvm::StringRef File, const ThreadsafeFS &TFS, 231 const ClangdLSPServer::Options &Opts) { 232 llvm::SmallString<0> FakeFile; 233 llvm::Optional<std::string> Contents; 234 if (File.empty()) { 235 llvm::sys::path::system_temp_directory(false, FakeFile); 236 llvm::sys::path::append(FakeFile, "test.cc"); 237 File = FakeFile; 238 Contents = R"cpp( 239 #include <stddef.h> 240 #include <string> 241 242 size_t N = 50; 243 auto xxx = std::string(N, 'x'); 244 )cpp"; 245 } 246 log("Testing on source file {0}", File); 247 248 Checker C(File, Opts); 249 if (!C.buildCommand() || !C.buildInvocation(TFS, Contents) || !C.buildAST()) 250 return false; 251 C.testLocationFeatures(); 252 253 log("All checks completed, {0} errors", C.ErrCount); 254 return C.ErrCount == 0; 255 } 256 257 } // namespace clangd 258 } // namespace clang 259