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 "CompileCommands.h" 30 #include "Config.h" 31 #include "GlobalCompilationDatabase.h" 32 #include "Hover.h" 33 #include "InlayHints.h" 34 #include "ParsedAST.h" 35 #include "Preamble.h" 36 #include "Protocol.h" 37 #include "SourceCode.h" 38 #include "XRefs.h" 39 #include "index/CanonicalIncludes.h" 40 #include "index/FileIndex.h" 41 #include "refactor/Tweak.h" 42 #include "support/ThreadsafeFS.h" 43 #include "support/Trace.h" 44 #include "clang/AST/ASTContext.h" 45 #include "clang/Format/Format.h" 46 #include "clang/Frontend/CompilerInvocation.h" 47 #include "clang/Tooling/CompilationDatabase.h" 48 #include "llvm/ADT/ArrayRef.h" 49 #include "llvm/ADT/Optional.h" 50 #include "llvm/Support/Path.h" 51 52 namespace clang { 53 namespace clangd { 54 namespace { 55 56 // Print (and count) the error-level diagnostics (warnings are ignored). 57 unsigned showErrors(llvm::ArrayRef<Diag> Diags) { 58 unsigned ErrCount = 0; 59 for (const auto &D : Diags) { 60 if (D.Severity >= DiagnosticsEngine::Error) { 61 elog("[{0}] Line {1}: {2}", D.Name, D.Range.start.line + 1, D.Message); 62 ++ErrCount; 63 } 64 } 65 return ErrCount; 66 } 67 68 // This class is just a linear pipeline whose functions get called in sequence. 69 // Each exercises part of clangd's logic on our test file and logs results. 70 // Later steps depend on state built in earlier ones (such as the AST). 71 // Many steps can fatally fail (return false), then subsequent ones cannot run. 72 // Nonfatal failures are logged and tracked in ErrCount. 73 class Checker { 74 // from constructor 75 std::string File; 76 ClangdLSPServer::Options Opts; 77 // from buildCommand 78 tooling::CompileCommand Cmd; 79 // from buildInvocation 80 ParseInputs Inputs; 81 std::unique_ptr<CompilerInvocation> Invocation; 82 format::FormatStyle Style; 83 // from buildAST 84 std::shared_ptr<const PreambleData> Preamble; 85 llvm::Optional<ParsedAST> AST; 86 FileIndex Index; 87 88 public: 89 // Number of non-fatal errors seen. 90 unsigned ErrCount = 0; 91 92 Checker(llvm::StringRef File, const ClangdLSPServer::Options &Opts) 93 : File(File), Opts(Opts) {} 94 95 // Read compilation database and choose a compile command for the file. 96 bool buildCommand(const ThreadsafeFS &TFS) { 97 log("Loading compilation database..."); 98 DirectoryBasedGlobalCompilationDatabase::Options CDBOpts(TFS); 99 CDBOpts.CompileCommandsDir = 100 Config::current().CompileFlags.CDBSearch.FixedCDBPath; 101 std::unique_ptr<GlobalCompilationDatabase> BaseCDB = 102 std::make_unique<DirectoryBasedGlobalCompilationDatabase>(CDBOpts); 103 BaseCDB = getQueryDriverDatabase(llvm::makeArrayRef(Opts.QueryDriverGlobs), 104 std::move(BaseCDB)); 105 auto Mangler = CommandMangler::detect(); 106 if (Opts.ResourceDir) 107 Mangler.ResourceDir = *Opts.ResourceDir; 108 auto CDB = std::make_unique<OverlayCDB>( 109 BaseCDB.get(), std::vector<std::string>{}, 110 tooling::ArgumentsAdjuster(std::move(Mangler))); 111 112 if (auto TrueCmd = CDB->getCompileCommand(File)) { 113 Cmd = std::move(*TrueCmd); 114 log("Compile command {0} is: {1}", 115 Cmd.Heuristic.empty() ? "from CDB" : Cmd.Heuristic, 116 printArgv(Cmd.CommandLine)); 117 } else { 118 Cmd = CDB->getFallbackCommand(File); 119 log("Generic fallback command is: {0}", printArgv(Cmd.CommandLine)); 120 } 121 122 return true; 123 } 124 125 // Prepare inputs and build CompilerInvocation (parsed compile command). 126 bool buildInvocation(const ThreadsafeFS &TFS, 127 llvm::Optional<std::string> Contents) { 128 StoreDiags CaptureInvocationDiags; 129 std::vector<std::string> CC1Args; 130 Inputs.CompileCommand = Cmd; 131 Inputs.TFS = &TFS; 132 Inputs.ClangTidyProvider = Opts.ClangTidyProvider; 133 Inputs.Opts.PreambleParseForwardingFunctions = 134 Opts.PreambleParseForwardingFunctions; 135 if (Contents) { 136 Inputs.Contents = *Contents; 137 log("Imaginary source file contents:\n{0}", Inputs.Contents); 138 } else { 139 if (auto Contents = TFS.view(llvm::None)->getBufferForFile(File)) { 140 Inputs.Contents = Contents->get()->getBuffer().str(); 141 } else { 142 elog("Couldn't read {0}: {1}", File, Contents.getError().message()); 143 return false; 144 } 145 } 146 log("Parsing command..."); 147 Invocation = 148 buildCompilerInvocation(Inputs, CaptureInvocationDiags, &CC1Args); 149 auto InvocationDiags = CaptureInvocationDiags.take(); 150 ErrCount += showErrors(InvocationDiags); 151 log("internal (cc1) args are: {0}", printArgv(CC1Args)); 152 if (!Invocation) { 153 elog("Failed to parse command line"); 154 return false; 155 } 156 157 // FIXME: Check that resource-dir/built-in-headers exist? 158 159 Style = getFormatStyleForFile(File, Inputs.Contents, TFS); 160 161 return true; 162 } 163 164 // Build preamble and AST, and index them. 165 bool buildAST() { 166 log("Building preamble..."); 167 Preamble = buildPreamble(File, *Invocation, Inputs, /*StoreInMemory=*/true, 168 [&](ASTContext &Ctx, Preprocessor &PP, 169 const CanonicalIncludes &Includes) { 170 if (!Opts.BuildDynamicSymbolIndex) 171 return; 172 log("Indexing headers..."); 173 Index.updatePreamble(File, /*Version=*/"null", 174 Ctx, PP, Includes); 175 }); 176 if (!Preamble) { 177 elog("Failed to build preamble"); 178 return false; 179 } 180 ErrCount += showErrors(Preamble->Diags); 181 182 log("Building AST..."); 183 AST = ParsedAST::build(File, Inputs, std::move(Invocation), 184 /*InvocationDiags=*/std::vector<Diag>{}, Preamble); 185 if (!AST) { 186 elog("Failed to build AST"); 187 return false; 188 } 189 ErrCount += showErrors(llvm::makeArrayRef(*AST->getDiagnostics()) 190 .drop_front(Preamble->Diags.size())); 191 192 if (Opts.BuildDynamicSymbolIndex) { 193 log("Indexing AST..."); 194 Index.updateMain(File, *AST); 195 } 196 return true; 197 } 198 199 // Build Inlay Hints for the entire AST or the specified range 200 void buildInlayHints(llvm::Optional<Range> LineRange) { 201 log("Building inlay hints"); 202 auto Hints = inlayHints(*AST, LineRange); 203 204 for (const auto &Hint : Hints) { 205 vlog(" {0} {1} {2}", Hint.kind, Hint.position, Hint.label); 206 } 207 } 208 209 // Run AST-based features at each token in the file. 210 void testLocationFeatures(llvm::Optional<Range> LineRange, 211 const bool EnableCodeCompletion) { 212 trace::Span Trace("testLocationFeatures"); 213 log("Testing features at each token (may be slow in large files)"); 214 auto &SM = AST->getSourceManager(); 215 auto SpelledTokens = AST->getTokens().spelledTokens(SM.getMainFileID()); 216 217 CodeCompleteOptions CCOpts = Opts.CodeComplete; 218 CCOpts.Index = &Index; 219 220 for (const auto &Tok : SpelledTokens) { 221 unsigned Start = AST->getSourceManager().getFileOffset(Tok.location()); 222 unsigned End = Start + Tok.length(); 223 Position Pos = offsetToPosition(Inputs.Contents, Start); 224 225 if (LineRange && !LineRange->contains(Pos)) 226 continue; 227 228 trace::Span Trace("Token"); 229 SPAN_ATTACH(Trace, "pos", Pos); 230 SPAN_ATTACH(Trace, "text", Tok.text(AST->getSourceManager())); 231 232 // FIXME: dumping the tokens may leak sensitive code into bug reports. 233 // Add an option to turn this off, once we decide how options work. 234 vlog(" {0} {1}", Pos, Tok.text(AST->getSourceManager())); 235 auto Tree = SelectionTree::createRight(AST->getASTContext(), 236 AST->getTokens(), Start, End); 237 Tweak::Selection Selection(&Index, *AST, Start, End, std::move(Tree), 238 nullptr); 239 // FS is only populated when applying a tweak, not during prepare as 240 // prepare should not do any I/O to be fast. 241 auto Tweaks = 242 prepareTweaks(Selection, Opts.TweakFilter, Opts.FeatureModules); 243 Selection.FS = 244 &AST->getSourceManager().getFileManager().getVirtualFileSystem(); 245 for (const auto &T : Tweaks) { 246 auto Result = T->apply(Selection); 247 if (!Result) { 248 elog(" tweak: {0} ==> FAIL: {1}", T->id(), Result.takeError()); 249 ++ErrCount; 250 } else { 251 vlog(" tweak: {0}", T->id()); 252 } 253 } 254 unsigned Definitions = locateSymbolAt(*AST, Pos, &Index).size(); 255 vlog(" definition: {0}", Definitions); 256 257 auto Hover = getHover(*AST, Pos, Style, &Index); 258 vlog(" hover: {0}", Hover.has_value()); 259 260 unsigned DocHighlights = findDocumentHighlights(*AST, Pos).size(); 261 vlog(" documentHighlight: {0}", DocHighlights); 262 263 if (EnableCodeCompletion) { 264 Position EndPos = offsetToPosition(Inputs.Contents, End); 265 auto CC = codeComplete(File, EndPos, Preamble.get(), Inputs, CCOpts); 266 vlog(" code completion: {0}", 267 CC.Completions.empty() ? "<empty>" : CC.Completions[0].Name); 268 } 269 } 270 } 271 }; 272 273 } // namespace 274 275 bool check(llvm::StringRef File, llvm::Optional<Range> LineRange, 276 const ThreadsafeFS &TFS, const ClangdLSPServer::Options &Opts, 277 bool EnableCodeCompletion) { 278 llvm::SmallString<0> FakeFile; 279 llvm::Optional<std::string> Contents; 280 if (File.empty()) { 281 llvm::sys::path::system_temp_directory(false, FakeFile); 282 llvm::sys::path::append(FakeFile, "test.cc"); 283 File = FakeFile; 284 Contents = R"cpp( 285 #include <stddef.h> 286 #include <string> 287 288 size_t N = 50; 289 auto xxx = std::string(N, 'x'); 290 )cpp"; 291 } 292 log("Testing on source file {0}", File); 293 294 auto ContextProvider = ClangdServer::createConfiguredContextProvider( 295 Opts.ConfigProvider, nullptr); 296 WithContext Ctx(ContextProvider( 297 FakeFile.empty() 298 ? File 299 : /*Don't turn on local configs for an arbitrary temp path.*/ "")); 300 Checker C(File, Opts); 301 if (!C.buildCommand(TFS) || !C.buildInvocation(TFS, Contents) || 302 !C.buildAST()) 303 return false; 304 C.buildInlayHints(LineRange); 305 C.testLocationFeatures(LineRange, EnableCodeCompletion); 306 307 log("All checks completed, {0} errors", C.ErrCount); 308 return C.ErrCount == 0; 309 } 310 311 } // namespace clangd 312 } // namespace clang 313