diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp index dfd26ad40b89..c2915aeada4f 100644 --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -1,1599 +1,1592 @@ //===--- ClangdLSPServer.cpp - LSP server ------------------------*- C++-*-===// // // 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 "ClangdLSPServer.h" #include "CodeComplete.h" #include "Diagnostics.h" #include "DraftStore.h" #include "GlobalCompilationDatabase.h" #include "Protocol.h" #include "SemanticHighlighting.h" #include "SourceCode.h" #include "TUScheduler.h" #include "URI.h" #include "refactor/Tweak.h" #include "support/Context.h" #include "support/Trace.h" #include "clang/Basic/Version.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/Optional.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/iterator_range.h" #include "llvm/Support/Errc.h" #include "llvm/Support/Error.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/JSON.h" #include "llvm/Support/Path.h" #include "llvm/Support/SHA1.h" #include "llvm/Support/ScopedPrinter.h" #include #include #include #include #include namespace clang { namespace clangd { namespace { // Tracks end-to-end latency of high level lsp calls. Measurements are in // seconds. constexpr trace::Metric LSPLatency("lsp_latency", trace::Metric::Distribution, "method_name"); // LSP defines file versions as numbers that increase. // ClangdServer treats them as opaque and therefore uses strings instead. std::string encodeVersion(int64_t LSPVersion) { return llvm::to_string(LSPVersion); } llvm::Optional decodeVersion(llvm::StringRef Encoded) { int64_t Result; if (llvm::to_integer(Encoded, Result, 10)) return Result; if (!Encoded.empty()) // Empty can be e.g. diagnostics on close. elog("unexpected non-numeric version {0}", Encoded); return llvm::None; } /// Transforms a tweak into a code action that would apply it if executed. /// EXPECTS: T.prepare() was called and returned true. CodeAction toCodeAction(const ClangdServer::TweakRef &T, const URIForFile &File, Range Selection) { CodeAction CA; CA.title = T.Title; - switch (T.Intent) { - case Tweak::Refactor: - CA.kind = std::string(CodeAction::REFACTOR_KIND); - break; - case Tweak::Info: - CA.kind = std::string(CodeAction::INFO_KIND); - break; - } + CA.kind = T.Kind.str(); // This tweak may have an expensive second stage, we only run it if the user // actually chooses it in the UI. We reply with a command that would run the // corresponding tweak. // FIXME: for some tweaks, computing the edits is cheap and we could send them // directly. CA.command.emplace(); CA.command->title = T.Title; CA.command->command = std::string(Command::CLANGD_APPLY_TWEAK); CA.command->tweakArgs.emplace(); CA.command->tweakArgs->file = File; CA.command->tweakArgs->tweakID = T.ID; CA.command->tweakArgs->selection = Selection; return CA; } void adjustSymbolKinds(llvm::MutableArrayRef Syms, SymbolKindBitset Kinds) { for (auto &S : Syms) { S.kind = adjustKindToCapability(S.kind, Kinds); adjustSymbolKinds(S.children, Kinds); } } SymbolKindBitset defaultSymbolKinds() { SymbolKindBitset Defaults; for (size_t I = SymbolKindMin; I <= static_cast(SymbolKind::Array); ++I) Defaults.set(I); return Defaults; } CompletionItemKindBitset defaultCompletionItemKinds() { CompletionItemKindBitset Defaults; for (size_t I = CompletionItemKindMin; I <= static_cast(CompletionItemKind::Reference); ++I) Defaults.set(I); return Defaults; } // Build a lookup table (HighlightingKind => {TextMate Scopes}), which is sent // to the LSP client. std::vector> buildHighlightScopeLookupTable() { std::vector> LookupTable; // HighlightingKind is using as the index. for (int KindValue = 0; KindValue <= (int)HighlightingKind::LastKind; ++KindValue) LookupTable.push_back( {std::string(toTextMateScope((HighlightingKind)(KindValue)))}); return LookupTable; } // Makes sure edits in \p FE are applicable to latest file contents reported by // editor. If not generates an error message containing information about files // that needs to be saved. llvm::Error validateEdits(const DraftStore &DraftMgr, const FileEdits &FE) { size_t InvalidFileCount = 0; llvm::StringRef LastInvalidFile; for (const auto &It : FE) { if (auto Draft = DraftMgr.getDraft(It.first())) { // If the file is open in user's editor, make sure the version we // saw and current version are compatible as this is the text that // will be replaced by editors. if (!It.second.canApplyTo(Draft->Contents)) { ++InvalidFileCount; LastInvalidFile = It.first(); } } } if (!InvalidFileCount) return llvm::Error::success(); if (InvalidFileCount == 1) return error("File must be saved first: {0}", LastInvalidFile); return error("Files must be saved first: {0} (and {1} others)", LastInvalidFile, InvalidFileCount - 1); } } // namespace // MessageHandler dispatches incoming LSP messages. // It handles cross-cutting concerns: // - serializes/deserializes protocol objects to JSON // - logging of inbound messages // - cancellation handling // - basic call tracing // MessageHandler ensures that initialize() is called before any other handler. class ClangdLSPServer::MessageHandler : public Transport::MessageHandler { public: MessageHandler(ClangdLSPServer &Server) : Server(Server) {} bool onNotify(llvm::StringRef Method, llvm::json::Value Params) override { WithContext HandlerContext(handlerContext()); log("<-- {0}", Method); if (Method == "exit") return false; if (!Server.Server) elog("Notification {0} before initialization", Method); else if (Method == "$/cancelRequest") onCancel(std::move(Params)); else if (auto Handler = Notifications.lookup(Method)) Handler(std::move(Params)); else log("unhandled notification {0}", Method); return true; } bool onCall(llvm::StringRef Method, llvm::json::Value Params, llvm::json::Value ID) override { WithContext HandlerContext(handlerContext()); // Calls can be canceled by the client. Add cancellation context. WithContext WithCancel(cancelableRequestContext(ID)); trace::Span Tracer(Method, LSPLatency); SPAN_ATTACH(Tracer, "Params", Params); ReplyOnce Reply(ID, Method, &Server, Tracer.Args); log("<-- {0}({1})", Method, ID); if (!Server.Server && Method != "initialize") { elog("Call {0} before initialization.", Method); Reply(llvm::make_error("server not initialized", ErrorCode::ServerNotInitialized)); } else if (auto Handler = Calls.lookup(Method)) Handler(std::move(Params), std::move(Reply)); else Reply(llvm::make_error("method not found", ErrorCode::MethodNotFound)); return true; } bool onReply(llvm::json::Value ID, llvm::Expected Result) override { WithContext HandlerContext(handlerContext()); Callback ReplyHandler = nullptr; if (auto IntID = ID.getAsInteger()) { std::lock_guard Mutex(CallMutex); // Find a corresponding callback for the request ID; for (size_t Index = 0; Index < ReplyCallbacks.size(); ++Index) { if (ReplyCallbacks[Index].first == *IntID) { ReplyHandler = std::move(ReplyCallbacks[Index].second); ReplyCallbacks.erase(ReplyCallbacks.begin() + Index); // remove the entry break; } } } if (!ReplyHandler) { // No callback being found, use a default log callback. ReplyHandler = [&ID](llvm::Expected Result) { elog("received a reply with ID {0}, but there was no such call", ID); if (!Result) llvm::consumeError(Result.takeError()); }; } // Log and run the reply handler. if (Result) { log("<-- reply({0})", ID); ReplyHandler(std::move(Result)); } else { auto Err = Result.takeError(); log("<-- reply({0}) error: {1}", ID, Err); ReplyHandler(std::move(Err)); } return true; } // Bind an LSP method name to a call. template void bind(const char *Method, void (ClangdLSPServer::*Handler)(const Param &, Callback)) { Calls[Method] = [Method, Handler, this](llvm::json::Value RawParams, ReplyOnce Reply) { auto P = parse(RawParams, Method, "request"); if (!P) return Reply(P.takeError()); (Server.*Handler)(*P, std::move(Reply)); }; } // Bind a reply callback to a request. The callback will be invoked when // clangd receives the reply from the LSP client. // Return a call id of the request. llvm::json::Value bindReply(Callback Reply) { llvm::Optional>> OldestCB; int ID; { std::lock_guard Mutex(CallMutex); ID = NextCallID++; ReplyCallbacks.emplace_back(ID, std::move(Reply)); // If the queue overflows, we assume that the client didn't reply the // oldest request, and run the corresponding callback which replies an // error to the client. if (ReplyCallbacks.size() > MaxReplayCallbacks) { elog("more than {0} outstanding LSP calls, forgetting about {1}", MaxReplayCallbacks, ReplyCallbacks.front().first); OldestCB = std::move(ReplyCallbacks.front()); ReplyCallbacks.pop_front(); } } if (OldestCB) OldestCB->second( error("failed to receive a client reply for request ({0})", OldestCB->first)); return ID; } // Bind an LSP method name to a notification. template void bind(const char *Method, void (ClangdLSPServer::*Handler)(const Param &)) { Notifications[Method] = [Method, Handler, this](llvm::json::Value RawParams) { llvm::Expected P = parse(RawParams, Method, "request"); if (!P) return llvm::consumeError(P.takeError()); trace::Span Tracer(Method, LSPLatency); SPAN_ATTACH(Tracer, "Params", RawParams); (Server.*Handler)(*P); }; } private: // Function object to reply to an LSP call. // Each instance must be called exactly once, otherwise: // - the bug is logged, and (in debug mode) an assert will fire // - if there was no reply, an error reply is sent // - if there were multiple replies, only the first is sent class ReplyOnce { std::atomic Replied = {false}; std::chrono::steady_clock::time_point Start; llvm::json::Value ID; std::string Method; ClangdLSPServer *Server; // Null when moved-from. llvm::json::Object *TraceArgs; public: ReplyOnce(const llvm::json::Value &ID, llvm::StringRef Method, ClangdLSPServer *Server, llvm::json::Object *TraceArgs) : Start(std::chrono::steady_clock::now()), ID(ID), Method(Method), Server(Server), TraceArgs(TraceArgs) { assert(Server); } ReplyOnce(ReplyOnce &&Other) : Replied(Other.Replied.load()), Start(Other.Start), ID(std::move(Other.ID)), Method(std::move(Other.Method)), Server(Other.Server), TraceArgs(Other.TraceArgs) { Other.Server = nullptr; } ReplyOnce &operator=(ReplyOnce &&) = delete; ReplyOnce(const ReplyOnce &) = delete; ReplyOnce &operator=(const ReplyOnce &) = delete; ~ReplyOnce() { // There's one legitimate reason to never reply to a request: clangd's // request handler send a call to the client (e.g. applyEdit) and the // client never replied. In this case, the ReplyOnce is owned by // ClangdLSPServer's reply callback table and is destroyed along with the // server. We don't attempt to send a reply in this case, there's little // to be gained from doing so. if (Server && !Server->IsBeingDestroyed && !Replied) { elog("No reply to message {0}({1})", Method, ID); assert(false && "must reply to all calls!"); (*this)(llvm::make_error("server failed to reply", ErrorCode::InternalError)); } } void operator()(llvm::Expected Reply) { assert(Server && "moved-from!"); if (Replied.exchange(true)) { elog("Replied twice to message {0}({1})", Method, ID); assert(false && "must reply to each call only once!"); return; } auto Duration = std::chrono::steady_clock::now() - Start; if (Reply) { log("--> reply:{0}({1}) {2:ms}", Method, ID, Duration); if (TraceArgs) (*TraceArgs)["Reply"] = *Reply; std::lock_guard Lock(Server->TranspWriter); Server->Transp.reply(std::move(ID), std::move(Reply)); } else { llvm::Error Err = Reply.takeError(); log("--> reply:{0}({1}) {2:ms}, error: {3}", Method, ID, Duration, Err); if (TraceArgs) (*TraceArgs)["Error"] = llvm::to_string(Err); std::lock_guard Lock(Server->TranspWriter); Server->Transp.reply(std::move(ID), std::move(Err)); } } }; llvm::StringMap> Notifications; llvm::StringMap> Calls; // Method calls may be cancelled by ID, so keep track of their state. // This needs a mutex: handlers may finish on a different thread, and that's // when we clean up entries in the map. mutable std::mutex RequestCancelersMutex; llvm::StringMap> RequestCancelers; unsigned NextRequestCookie = 0; // To disambiguate reused IDs, see below. void onCancel(const llvm::json::Value &Params) { const llvm::json::Value *ID = nullptr; if (auto *O = Params.getAsObject()) ID = O->get("id"); if (!ID) { elog("Bad cancellation request: {0}", Params); return; } auto StrID = llvm::to_string(*ID); std::lock_guard Lock(RequestCancelersMutex); auto It = RequestCancelers.find(StrID); if (It != RequestCancelers.end()) It->second.first(); // Invoke the canceler. } Context handlerContext() const { return Context::current().derive( kCurrentOffsetEncoding, Server.Opts.Encoding.getValueOr(OffsetEncoding::UTF16)); } // We run cancelable requests in a context that does two things: // - allows cancellation using RequestCancelers[ID] // - cleans up the entry in RequestCancelers when it's no longer needed // If a client reuses an ID, the last wins and the first cannot be canceled. Context cancelableRequestContext(const llvm::json::Value &ID) { auto Task = cancelableTask( /*Reason=*/static_cast(ErrorCode::RequestCancelled)); auto StrID = llvm::to_string(ID); // JSON-serialize ID for map key. auto Cookie = NextRequestCookie++; // No lock, only called on main thread. { std::lock_guard Lock(RequestCancelersMutex); RequestCancelers[StrID] = {std::move(Task.second), Cookie}; } // When the request ends, we can clean up the entry we just added. // The cookie lets us check that it hasn't been overwritten due to ID // reuse. return Task.first.derive(llvm::make_scope_exit([this, StrID, Cookie] { std::lock_guard Lock(RequestCancelersMutex); auto It = RequestCancelers.find(StrID); if (It != RequestCancelers.end() && It->second.second == Cookie) RequestCancelers.erase(It); })); } // The maximum number of callbacks held in clangd. // // We bound the maximum size to the pending map to prevent memory leakage // for cases where LSP clients don't reply for the request. // This has to go after RequestCancellers and RequestCancellersMutex since it // can contain a callback that has a cancelable context. static constexpr int MaxReplayCallbacks = 100; mutable std::mutex CallMutex; int NextCallID = 0; /* GUARDED_BY(CallMutex) */ std::deque>> ReplyCallbacks; /* GUARDED_BY(CallMutex) */ ClangdLSPServer &Server; }; constexpr int ClangdLSPServer::MessageHandler::MaxReplayCallbacks; // call(), notify(), and reply() wrap the Transport, adding logging and locking. void ClangdLSPServer::callRaw(StringRef Method, llvm::json::Value Params, Callback CB) { auto ID = MsgHandler->bindReply(std::move(CB)); log("--> {0}({1})", Method, ID); std::lock_guard Lock(TranspWriter); Transp.call(Method, std::move(Params), ID); } void ClangdLSPServer::notify(llvm::StringRef Method, llvm::json::Value Params) { log("--> {0}", Method); std::lock_guard Lock(TranspWriter); Transp.notify(Method, std::move(Params)); } static std::vector semanticTokenTypes() { std::vector Types; for (unsigned I = 0; I <= static_cast(HighlightingKind::LastKind); ++I) Types.push_back(toSemanticTokenType(static_cast(I))); return Types; } void ClangdLSPServer::onInitialize(const InitializeParams &Params, Callback Reply) { // Determine character encoding first as it affects constructed ClangdServer. if (Params.capabilities.offsetEncoding && !Opts.Encoding) { Opts.Encoding = OffsetEncoding::UTF16; // fallback for (OffsetEncoding Supported : *Params.capabilities.offsetEncoding) if (Supported != OffsetEncoding::UnsupportedEncoding) { Opts.Encoding = Supported; break; } } Opts.TheiaSemanticHighlighting = Params.capabilities.TheiaSemanticHighlighting; if (Params.capabilities.TheiaSemanticHighlighting && Params.capabilities.SemanticTokens) { log("Client supports legacy semanticHighlights notification and standard " "semanticTokens request, choosing the latter (no notifications)."); Opts.TheiaSemanticHighlighting = false; } if (Params.rootUri && *Params.rootUri) Opts.WorkspaceRoot = std::string(Params.rootUri->file()); else if (Params.rootPath && !Params.rootPath->empty()) Opts.WorkspaceRoot = *Params.rootPath; if (Server) return Reply(llvm::make_error("server already initialized", ErrorCode::InvalidRequest)); if (const auto &Dir = Params.initializationOptions.compilationDatabasePath) Opts.CompileCommandsDir = Dir; if (Opts.UseDirBasedCDB) { BaseCDB = std::make_unique( Opts.CompileCommandsDir); BaseCDB = getQueryDriverDatabase(llvm::makeArrayRef(Opts.QueryDriverGlobs), std::move(BaseCDB)); } auto Mangler = CommandMangler::detect(); if (Opts.ResourceDir) Mangler.ResourceDir = *Opts.ResourceDir; CDB.emplace(BaseCDB.get(), Params.initializationOptions.fallbackFlags, tooling::ArgumentsAdjuster(std::move(Mangler))); { // Switch caller's context with LSPServer's background context. Since we // rather want to propagate information from LSPServer's context into the // Server, CDB, etc. WithContext MainContext(BackgroundContext.clone()); llvm::Optional WithOffsetEncoding; if (Opts.Encoding) WithOffsetEncoding.emplace(kCurrentOffsetEncoding, *Opts.Encoding); Server.emplace(*CDB, TFS, Opts, static_cast(this)); } applyConfiguration(Params.initializationOptions.ConfigSettings); Opts.CodeComplete.EnableSnippets = Params.capabilities.CompletionSnippets; Opts.CodeComplete.IncludeFixIts = Params.capabilities.CompletionFixes; if (!Opts.CodeComplete.BundleOverloads.hasValue()) Opts.CodeComplete.BundleOverloads = Params.capabilities.HasSignatureHelp; Opts.CodeComplete.DocumentationFormat = Params.capabilities.CompletionDocumentationFormat; DiagOpts.EmbedFixesInDiagnostics = Params.capabilities.DiagnosticFixes; DiagOpts.SendDiagnosticCategory = Params.capabilities.DiagnosticCategory; DiagOpts.EmitRelatedLocations = Params.capabilities.DiagnosticRelatedInformation; if (Params.capabilities.WorkspaceSymbolKinds) SupportedSymbolKinds |= *Params.capabilities.WorkspaceSymbolKinds; if (Params.capabilities.CompletionItemKinds) SupportedCompletionItemKinds |= *Params.capabilities.CompletionItemKinds; SupportsCodeAction = Params.capabilities.CodeActionStructure; SupportsHierarchicalDocumentSymbol = Params.capabilities.HierarchicalDocumentSymbol; SupportFileStatus = Params.initializationOptions.FileStatus; HoverContentFormat = Params.capabilities.HoverContentFormat; SupportsOffsetsInSignatureHelp = Params.capabilities.OffsetsInSignatureHelp; if (Params.capabilities.WorkDoneProgress) BackgroundIndexProgressState = BackgroundIndexProgress::Empty; BackgroundIndexSkipCreate = Params.capabilities.ImplicitProgressCreation; // Per LSP, renameProvider can be either boolean or RenameOptions. // RenameOptions will be specified if the client states it supports prepare. llvm::json::Value RenameProvider = llvm::json::Object{{"prepareProvider", true}}; if (!Params.capabilities.RenamePrepareSupport) // Only boolean allowed per LSP RenameProvider = true; // Per LSP, codeActionProvide can be either boolean or CodeActionOptions. // CodeActionOptions is only valid if the client supports action literal // via textDocument.codeAction.codeActionLiteralSupport. llvm::json::Value CodeActionProvider = true; if (Params.capabilities.CodeActionStructure) CodeActionProvider = llvm::json::Object{ {"codeActionKinds", {CodeAction::QUICKFIX_KIND, CodeAction::REFACTOR_KIND, CodeAction::INFO_KIND}}}; llvm::json::Object Result{ {{"serverInfo", llvm::json::Object{{"name", "clangd"}, {"version", getClangToolFullVersion("clangd")}}}, {"capabilities", llvm::json::Object{ {"textDocumentSync", llvm::json::Object{ {"openClose", true}, {"change", (int)TextDocumentSyncKind::Incremental}, {"save", true}, }}, {"documentFormattingProvider", true}, {"documentRangeFormattingProvider", true}, {"documentOnTypeFormattingProvider", llvm::json::Object{ {"firstTriggerCharacter", "\n"}, {"moreTriggerCharacter", {}}, }}, {"codeActionProvider", std::move(CodeActionProvider)}, {"completionProvider", llvm::json::Object{ {"allCommitCharacters", {" ", "\t", "(", ")", "[", "]", "{", "}", "<", ">", ":", ";", ",", "+", "-", "/", "*", "%", "^", "&", "#", "?", ".", "=", "\"", "'", "|"}}, {"resolveProvider", false}, // We do extra checks, e.g. that > is part of ->. {"triggerCharacters", {".", "<", ">", ":", "\"", "/"}}, }}, {"semanticTokensProvider", llvm::json::Object{ {"full", llvm::json::Object{{"delta", true}}}, {"range", false}, {"legend", llvm::json::Object{{"tokenTypes", semanticTokenTypes()}, {"tokenModifiers", llvm::json::Array()}}}, }}, {"signatureHelpProvider", llvm::json::Object{ {"triggerCharacters", {"(", ","}}, }}, {"declarationProvider", true}, {"definitionProvider", true}, {"documentHighlightProvider", true}, {"documentLinkProvider", llvm::json::Object{ {"resolveProvider", false}, }}, {"hoverProvider", true}, {"renameProvider", std::move(RenameProvider)}, {"selectionRangeProvider", true}, {"documentSymbolProvider", true}, {"workspaceSymbolProvider", true}, {"referencesProvider", true}, {"executeCommandProvider", llvm::json::Object{ {"commands", {ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND, ExecuteCommandParams::CLANGD_APPLY_TWEAK}}, }}, {"typeHierarchyProvider", true}, }}}}; if (Opts.Encoding) Result["offsetEncoding"] = *Opts.Encoding; if (Opts.TheiaSemanticHighlighting) Result.getObject("capabilities") ->insert( {"semanticHighlighting", llvm::json::Object{{"scopes", buildHighlightScopeLookupTable()}}}); if (Opts.FoldingRanges) Result.getObject("capabilities")->insert({"foldingRangeProvider", true}); Reply(std::move(Result)); } void ClangdLSPServer::onInitialized(const InitializedParams &Params) {} void ClangdLSPServer::onShutdown(const ShutdownParams &Params, Callback Reply) { // Do essentially nothing, just say we're ready to exit. ShutdownRequestReceived = true; Reply(nullptr); } // sync is a clangd extension: it blocks until all background work completes. // It blocks the calling thread, so no messages are processed until it returns! void ClangdLSPServer::onSync(const NoParams &Params, Callback Reply) { if (Server->blockUntilIdleForTest(/*TimeoutSeconds=*/60)) Reply(nullptr); else Reply(error("Not idle after a minute")); } void ClangdLSPServer::onDocumentDidOpen( const DidOpenTextDocumentParams &Params) { PathRef File = Params.textDocument.uri.file(); const std::string &Contents = Params.textDocument.text; auto Version = DraftMgr.addDraft(File, Params.textDocument.version, Contents); Server->addDocument(File, Contents, encodeVersion(Version), WantDiagnostics::Yes); } void ClangdLSPServer::onDocumentDidChange( const DidChangeTextDocumentParams &Params) { auto WantDiags = WantDiagnostics::Auto; if (Params.wantDiagnostics.hasValue()) WantDiags = Params.wantDiagnostics.getValue() ? WantDiagnostics::Yes : WantDiagnostics::No; PathRef File = Params.textDocument.uri.file(); llvm::Expected Draft = DraftMgr.updateDraft( File, Params.textDocument.version, Params.contentChanges); if (!Draft) { // If this fails, we are most likely going to be not in sync anymore with // the client. It is better to remove the draft and let further operations // fail rather than giving wrong results. DraftMgr.removeDraft(File); Server->removeDocument(File); elog("Failed to update {0}: {1}", File, Draft.takeError()); return; } Server->addDocument(File, Draft->Contents, encodeVersion(Draft->Version), WantDiags, Params.forceRebuild); } void ClangdLSPServer::onDocumentDidSave( const DidSaveTextDocumentParams &Params) { reparseOpenFilesIfNeeded([](llvm::StringRef) { return true; }); } void ClangdLSPServer::onFileEvent(const DidChangeWatchedFilesParams &Params) { // We could also reparse all open files here. However: // - this could be frequent, and revalidating all the preambles isn't free // - this is useful e.g. when switching git branches, but we're likely to see // fresh headers but still have the old-branch main-file content Server->onFileEvent(Params); } void ClangdLSPServer::onCommand(const ExecuteCommandParams &Params, Callback Reply) { auto ApplyEdit = [this](WorkspaceEdit WE, std::string SuccessMessage, decltype(Reply) Reply) { ApplyWorkspaceEditParams Edit; Edit.edit = std::move(WE); call( "workspace/applyEdit", std::move(Edit), [Reply = std::move(Reply), SuccessMessage = std::move(SuccessMessage)]( llvm::Expected Response) mutable { if (!Response) return Reply(Response.takeError()); if (!Response->applied) { std::string Reason = Response->failureReason ? *Response->failureReason : "unknown reason"; return Reply(error("edits were not applied: {0}", Reason)); } return Reply(SuccessMessage); }); }; if (Params.command == ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND && Params.workspaceEdit) { // The flow for "apply-fix" : // 1. We publish a diagnostic, including fixits // 2. The user clicks on the diagnostic, the editor asks us for code actions // 3. We send code actions, with the fixit embedded as context // 4. The user selects the fixit, the editor asks us to apply it // 5. We unwrap the changes and send them back to the editor // 6. The editor applies the changes (applyEdit), and sends us a reply // 7. We unwrap the reply and send a reply to the editor. ApplyEdit(*Params.workspaceEdit, "Fix applied.", std::move(Reply)); } else if (Params.command == ExecuteCommandParams::CLANGD_APPLY_TWEAK && Params.tweakArgs) { auto Code = DraftMgr.getDraft(Params.tweakArgs->file.file()); if (!Code) return Reply(error("trying to apply a code action for a non-added file")); auto Action = [this, ApplyEdit, Reply = std::move(Reply), File = Params.tweakArgs->file, Code = std::move(*Code)]( llvm::Expected R) mutable { if (!R) return Reply(R.takeError()); assert(R->ShowMessage || (!R->ApplyEdits.empty() && "tweak has no effect")); if (R->ShowMessage) { ShowMessageParams Msg; Msg.message = *R->ShowMessage; Msg.type = MessageType::Info; notify("window/showMessage", Msg); } // When no edit is specified, make sure we Reply(). if (R->ApplyEdits.empty()) return Reply("Tweak applied."); if (auto Err = validateEdits(DraftMgr, R->ApplyEdits)) return Reply(std::move(Err)); WorkspaceEdit WE; WE.changes.emplace(); for (const auto &It : R->ApplyEdits) { (*WE.changes)[URI::createFile(It.first()).toString()] = It.second.asTextEdits(); } // ApplyEdit will take care of calling Reply(). return ApplyEdit(std::move(WE), "Tweak applied.", std::move(Reply)); }; Server->applyTweak(Params.tweakArgs->file.file(), Params.tweakArgs->selection, Params.tweakArgs->tweakID, std::move(Action)); } else { // We should not get here because ExecuteCommandParams would not have // parsed in the first place and this handler should not be called. But if // more commands are added, this will be here has a safe guard. Reply(llvm::make_error( llvm::formatv("Unsupported command \"{0}\".", Params.command).str(), ErrorCode::InvalidParams)); } } void ClangdLSPServer::onWorkspaceSymbol( const WorkspaceSymbolParams &Params, Callback> Reply) { Server->workspaceSymbols( Params.query, Opts.CodeComplete.Limit, [Reply = std::move(Reply), this](llvm::Expected> Items) mutable { if (!Items) return Reply(Items.takeError()); for (auto &Sym : *Items) Sym.kind = adjustKindToCapability(Sym.kind, SupportedSymbolKinds); Reply(std::move(*Items)); }); } void ClangdLSPServer::onPrepareRename(const TextDocumentPositionParams &Params, Callback> Reply) { Server->prepareRename(Params.textDocument.uri.file(), Params.position, Opts.Rename, std::move(Reply)); } void ClangdLSPServer::onRename(const RenameParams &Params, Callback Reply) { Path File = std::string(Params.textDocument.uri.file()); if (!DraftMgr.getDraft(File)) return Reply(llvm::make_error( "onRename called for non-added file", ErrorCode::InvalidParams)); Server->rename( File, Params.position, Params.newName, Opts.Rename, [File, Params, Reply = std::move(Reply), this](llvm::Expected Edits) mutable { if (!Edits) return Reply(Edits.takeError()); if (auto Err = validateEdits(DraftMgr, *Edits)) return Reply(std::move(Err)); WorkspaceEdit Result; Result.changes.emplace(); for (const auto &Rep : *Edits) { (*Result.changes)[URI::createFile(Rep.first()).toString()] = Rep.second.asTextEdits(); } Reply(Result); }); } void ClangdLSPServer::onDocumentDidClose( const DidCloseTextDocumentParams &Params) { PathRef File = Params.textDocument.uri.file(); DraftMgr.removeDraft(File); Server->removeDocument(File); { std::lock_guard Lock(FixItsMutex); FixItsMap.erase(File); } { std::lock_guard HLock(HighlightingsMutex); FileToHighlightings.erase(File); } { std::lock_guard HLock(SemanticTokensMutex); LastSemanticTokens.erase(File); } // clangd will not send updates for this file anymore, so we empty out the // list of diagnostics shown on the client (e.g. in the "Problems" pane of // VSCode). Note that this cannot race with actual diagnostics responses // because removeDocument() guarantees no diagnostic callbacks will be // executed after it returns. PublishDiagnosticsParams Notification; Notification.uri = URIForFile::canonicalize(File, /*TUPath=*/File); publishDiagnostics(Notification); } void ClangdLSPServer::onDocumentOnTypeFormatting( const DocumentOnTypeFormattingParams &Params, Callback> Reply) { auto File = Params.textDocument.uri.file(); auto Code = DraftMgr.getDraft(File); if (!Code) return Reply(llvm::make_error( "onDocumentOnTypeFormatting called for non-added file", ErrorCode::InvalidParams)); Server->formatOnType(File, Code->Contents, Params.position, Params.ch, std::move(Reply)); } void ClangdLSPServer::onDocumentRangeFormatting( const DocumentRangeFormattingParams &Params, Callback> Reply) { auto File = Params.textDocument.uri.file(); auto Code = DraftMgr.getDraft(File); if (!Code) return Reply(llvm::make_error( "onDocumentRangeFormatting called for non-added file", ErrorCode::InvalidParams)); Server->formatRange( File, Code->Contents, Params.range, [Code = Code->Contents, Reply = std::move(Reply)]( llvm::Expected Result) mutable { if (Result) Reply(replacementsToEdits(Code, Result.get())); else Reply(Result.takeError()); }); } void ClangdLSPServer::onDocumentFormatting( const DocumentFormattingParams &Params, Callback> Reply) { auto File = Params.textDocument.uri.file(); auto Code = DraftMgr.getDraft(File); if (!Code) return Reply(llvm::make_error( "onDocumentFormatting called for non-added file", ErrorCode::InvalidParams)); Server->formatFile(File, Code->Contents, [Code = Code->Contents, Reply = std::move(Reply)]( llvm::Expected Result) mutable { if (Result) Reply(replacementsToEdits(Code, Result.get())); else Reply(Result.takeError()); }); } /// The functions constructs a flattened view of the DocumentSymbol hierarchy. /// Used by the clients that do not support the hierarchical view. static std::vector flattenSymbolHierarchy(llvm::ArrayRef Symbols, const URIForFile &FileURI) { std::vector Results; std::function Process = [&](const DocumentSymbol &S, llvm::Optional ParentName) { SymbolInformation SI; SI.containerName = std::string(ParentName ? "" : *ParentName); SI.name = S.name; SI.kind = S.kind; SI.location.range = S.range; SI.location.uri = FileURI; Results.push_back(std::move(SI)); std::string FullName = !ParentName ? S.name : (ParentName->str() + "::" + S.name); for (auto &C : S.children) Process(C, /*ParentName=*/FullName); }; for (auto &S : Symbols) Process(S, /*ParentName=*/""); return Results; } void ClangdLSPServer::onDocumentSymbol(const DocumentSymbolParams &Params, Callback Reply) { URIForFile FileURI = Params.textDocument.uri; Server->documentSymbols( Params.textDocument.uri.file(), [this, FileURI, Reply = std::move(Reply)]( llvm::Expected> Items) mutable { if (!Items) return Reply(Items.takeError()); adjustSymbolKinds(*Items, SupportedSymbolKinds); if (SupportsHierarchicalDocumentSymbol) return Reply(std::move(*Items)); else return Reply(flattenSymbolHierarchy(*Items, FileURI)); }); } void ClangdLSPServer::onFoldingRange( const FoldingRangeParams &Params, Callback> Reply) { Server->foldingRanges(Params.textDocument.uri.file(), std::move(Reply)); } static llvm::Optional asCommand(const CodeAction &Action) { Command Cmd; if (Action.command && Action.edit) return None; // Not representable. (We never emit these anyway). if (Action.command) { Cmd = *Action.command; } else if (Action.edit) { Cmd.command = std::string(Command::CLANGD_APPLY_FIX_COMMAND); Cmd.workspaceEdit = *Action.edit; } else { return None; } Cmd.title = Action.title; if (Action.kind && *Action.kind == CodeAction::QUICKFIX_KIND) Cmd.title = "Apply fix: " + Cmd.title; return Cmd; } void ClangdLSPServer::onCodeAction(const CodeActionParams &Params, Callback Reply) { URIForFile File = Params.textDocument.uri; auto Code = DraftMgr.getDraft(File.file()); if (!Code) return Reply(llvm::make_error( "onCodeAction called for non-added file", ErrorCode::InvalidParams)); // We provide a code action for Fixes on the specified diagnostics. std::vector FixIts; for (const Diagnostic &D : Params.context.diagnostics) { for (auto &F : getFixes(File.file(), D)) { FixIts.push_back(toCodeAction(F, Params.textDocument.uri)); FixIts.back().diagnostics = {D}; } } // Now enumerate the semantic code actions. auto ConsumeActions = [Reply = std::move(Reply), File, Code = std::move(*Code), Selection = Params.range, FixIts = std::move(FixIts), this]( llvm::Expected> Tweaks) mutable { if (!Tweaks) return Reply(Tweaks.takeError()); std::vector Actions = std::move(FixIts); Actions.reserve(Actions.size() + Tweaks->size()); for (const auto &T : *Tweaks) Actions.push_back(toCodeAction(T, File, Selection)); // If there's exactly one quick-fix, call it "preferred". // We never consider refactorings etc as preferred. CodeAction *OnlyFix = nullptr; for (auto &Action : Actions) { if (Action.kind && *Action.kind == CodeAction::QUICKFIX_KIND) { if (OnlyFix) { OnlyFix->isPreferred = false; break; } Action.isPreferred = true; OnlyFix = &Action; } } if (SupportsCodeAction) return Reply(llvm::json::Array(Actions)); std::vector Commands; for (const auto &Action : Actions) { if (auto Command = asCommand(Action)) Commands.push_back(std::move(*Command)); } return Reply(llvm::json::Array(Commands)); }; Server->enumerateTweaks(File.file(), Params.range, std::move(ConsumeActions)); } void ClangdLSPServer::onCompletion(const CompletionParams &Params, Callback Reply) { if (!shouldRunCompletion(Params)) { // Clients sometimes auto-trigger completions in undesired places (e.g. // 'a >^ '), we return empty results in those cases. vlog("ignored auto-triggered completion, preceding char did not match"); return Reply(CompletionList()); } Server->codeComplete(Params.textDocument.uri.file(), Params.position, Opts.CodeComplete, [Reply = std::move(Reply), this](llvm::Expected List) mutable { if (!List) return Reply(List.takeError()); CompletionList LSPList; LSPList.isIncomplete = List->HasMore; for (const auto &R : List->Completions) { CompletionItem C = R.render(Opts.CodeComplete); C.kind = adjustKindToCapability( C.kind, SupportedCompletionItemKinds); LSPList.items.push_back(std::move(C)); } return Reply(std::move(LSPList)); }); } void ClangdLSPServer::onSignatureHelp(const TextDocumentPositionParams &Params, Callback Reply) { Server->signatureHelp(Params.textDocument.uri.file(), Params.position, [Reply = std::move(Reply), this]( llvm::Expected Signature) mutable { if (!Signature) return Reply(Signature.takeError()); if (SupportsOffsetsInSignatureHelp) return Reply(std::move(*Signature)); // Strip out the offsets from signature help for // clients that only support string labels. for (auto &SigInfo : Signature->signatures) { for (auto &Param : SigInfo.parameters) Param.labelOffsets.reset(); } return Reply(std::move(*Signature)); }); } // Go to definition has a toggle function: if def and decl are distinct, then // the first press gives you the def, the second gives you the matching def. // getToggle() returns the counterpart location that under the cursor. // // We return the toggled location alone (ignoring other symbols) to encourage // editors to "bounce" quickly between locations, without showing a menu. static Location *getToggle(const TextDocumentPositionParams &Point, LocatedSymbol &Sym) { // Toggle only makes sense with two distinct locations. if (!Sym.Definition || *Sym.Definition == Sym.PreferredDeclaration) return nullptr; if (Sym.Definition->uri.file() == Point.textDocument.uri.file() && Sym.Definition->range.contains(Point.position)) return &Sym.PreferredDeclaration; if (Sym.PreferredDeclaration.uri.file() == Point.textDocument.uri.file() && Sym.PreferredDeclaration.range.contains(Point.position)) return &*Sym.Definition; return nullptr; } void ClangdLSPServer::onGoToDefinition(const TextDocumentPositionParams &Params, Callback> Reply) { Server->locateSymbolAt( Params.textDocument.uri.file(), Params.position, [Params, Reply = std::move(Reply)]( llvm::Expected> Symbols) mutable { if (!Symbols) return Reply(Symbols.takeError()); std::vector Defs; for (auto &S : *Symbols) { if (Location *Toggle = getToggle(Params, S)) return Reply(std::vector{std::move(*Toggle)}); Defs.push_back(S.Definition.getValueOr(S.PreferredDeclaration)); } Reply(std::move(Defs)); }); } void ClangdLSPServer::onGoToDeclaration( const TextDocumentPositionParams &Params, Callback> Reply) { Server->locateSymbolAt( Params.textDocument.uri.file(), Params.position, [Params, Reply = std::move(Reply)]( llvm::Expected> Symbols) mutable { if (!Symbols) return Reply(Symbols.takeError()); std::vector Decls; for (auto &S : *Symbols) { if (Location *Toggle = getToggle(Params, S)) return Reply(std::vector{std::move(*Toggle)}); Decls.push_back(std::move(S.PreferredDeclaration)); } Reply(std::move(Decls)); }); } void ClangdLSPServer::onSwitchSourceHeader( const TextDocumentIdentifier &Params, Callback> Reply) { Server->switchSourceHeader( Params.uri.file(), [Reply = std::move(Reply), Params](llvm::Expected> Path) mutable { if (!Path) return Reply(Path.takeError()); if (*Path) return Reply(URIForFile::canonicalize(**Path, Params.uri.file())); return Reply(llvm::None); }); } void ClangdLSPServer::onDocumentHighlight( const TextDocumentPositionParams &Params, Callback> Reply) { Server->findDocumentHighlights(Params.textDocument.uri.file(), Params.position, std::move(Reply)); } void ClangdLSPServer::onHover(const TextDocumentPositionParams &Params, Callback> Reply) { Server->findHover(Params.textDocument.uri.file(), Params.position, [Reply = std::move(Reply), this]( llvm::Expected> H) mutable { if (!H) return Reply(H.takeError()); if (!*H) return Reply(llvm::None); Hover R; R.contents.kind = HoverContentFormat; R.range = (*H)->SymRange; switch (HoverContentFormat) { case MarkupKind::PlainText: R.contents.value = (*H)->present().asPlainText(); return Reply(std::move(R)); case MarkupKind::Markdown: R.contents.value = (*H)->present().asMarkdown(); return Reply(std::move(R)); }; llvm_unreachable("unhandled MarkupKind"); }); } void ClangdLSPServer::onTypeHierarchy( const TypeHierarchyParams &Params, Callback> Reply) { Server->typeHierarchy(Params.textDocument.uri.file(), Params.position, Params.resolve, Params.direction, std::move(Reply)); } void ClangdLSPServer::onResolveTypeHierarchy( const ResolveTypeHierarchyItemParams &Params, Callback> Reply) { Server->resolveTypeHierarchy(Params.item, Params.resolve, Params.direction, std::move(Reply)); } void ClangdLSPServer::applyConfiguration( const ConfigurationSettings &Settings) { // Per-file update to the compilation database. llvm::StringSet<> ModifiedFiles; for (auto &Entry : Settings.compilationDatabaseChanges) { PathRef File = Entry.first; auto Old = CDB->getCompileCommand(File); auto New = tooling::CompileCommand(std::move(Entry.second.workingDirectory), File, std::move(Entry.second.compilationCommand), /*Output=*/""); if (Old != New) { CDB->setCompileCommand(File, std::move(New)); ModifiedFiles.insert(File); } } reparseOpenFilesIfNeeded( [&](llvm::StringRef File) { return ModifiedFiles.count(File) != 0; }); } void ClangdLSPServer::publishTheiaSemanticHighlighting( const TheiaSemanticHighlightingParams &Params) { notify("textDocument/semanticHighlighting", Params); } void ClangdLSPServer::publishDiagnostics( const PublishDiagnosticsParams &Params) { notify("textDocument/publishDiagnostics", Params); } // FIXME: This function needs to be properly tested. void ClangdLSPServer::onChangeConfiguration( const DidChangeConfigurationParams &Params) { applyConfiguration(Params.settings); } void ClangdLSPServer::onReference(const ReferenceParams &Params, Callback> Reply) { Server->findReferences(Params.textDocument.uri.file(), Params.position, Opts.CodeComplete.Limit, [Reply = std::move(Reply)]( llvm::Expected Refs) mutable { if (!Refs) return Reply(Refs.takeError()); return Reply(std::move(Refs->References)); }); } void ClangdLSPServer::onSymbolInfo(const TextDocumentPositionParams &Params, Callback> Reply) { Server->symbolInfo(Params.textDocument.uri.file(), Params.position, std::move(Reply)); } void ClangdLSPServer::onSelectionRange( const SelectionRangeParams &Params, Callback> Reply) { Server->semanticRanges( Params.textDocument.uri.file(), Params.positions, [Reply = std::move(Reply)]( llvm::Expected> Ranges) mutable { if (!Ranges) return Reply(Ranges.takeError()); return Reply(std::move(*Ranges)); }); } void ClangdLSPServer::onDocumentLink( const DocumentLinkParams &Params, Callback> Reply) { // TODO(forster): This currently resolves all targets eagerly. This is slow, // because it blocks on the preamble/AST being built. We could respond to the // request faster by using string matching or the lexer to find the includes // and resolving the targets lazily. Server->documentLinks( Params.textDocument.uri.file(), [Reply = std::move(Reply)]( llvm::Expected> Links) mutable { if (!Links) { return Reply(Links.takeError()); } return Reply(std::move(Links)); }); } // Increment a numeric string: "" -> 1 -> 2 -> ... -> 9 -> 10 -> 11 ... static void increment(std::string &S) { for (char &C : llvm::reverse(S)) { if (C != '9') { ++C; return; } C = '0'; } S.insert(S.begin(), '1'); } void ClangdLSPServer::onSemanticTokens(const SemanticTokensParams &Params, Callback CB) { Server->semanticHighlights( Params.textDocument.uri.file(), [this, File(Params.textDocument.uri.file().str()), CB(std::move(CB))]( llvm::Expected> HT) mutable { if (!HT) return CB(HT.takeError()); SemanticTokens Result; Result.tokens = toSemanticTokens(*HT); { std::lock_guard Lock(SemanticTokensMutex); auto &Last = LastSemanticTokens[File]; Last.tokens = Result.tokens; increment(Last.resultId); Result.resultId = Last.resultId; } CB(std::move(Result)); }); } void ClangdLSPServer::onSemanticTokensDelta( const SemanticTokensDeltaParams &Params, Callback CB) { Server->semanticHighlights( Params.textDocument.uri.file(), [this, PrevResultID(Params.previousResultId), File(Params.textDocument.uri.file().str()), CB(std::move(CB))]( llvm::Expected> HT) mutable { if (!HT) return CB(HT.takeError()); std::vector Toks = toSemanticTokens(*HT); SemanticTokensOrDelta Result; { std::lock_guard Lock(SemanticTokensMutex); auto &Last = LastSemanticTokens[File]; if (PrevResultID == Last.resultId) { Result.edits = diffTokens(Last.tokens, Toks); } else { vlog("semanticTokens/full/delta: wanted edits vs {0} but last " "result had ID {1}. Returning full token list.", PrevResultID, Last.resultId); Result.tokens = Toks; } Last.tokens = std::move(Toks); increment(Last.resultId); Result.resultId = Last.resultId; } CB(std::move(Result)); }); } ClangdLSPServer::ClangdLSPServer(class Transport &Transp, const ThreadsafeFS &TFS, const ClangdLSPServer::Options &Opts) : BackgroundContext(Context::current().clone()), Transp(Transp), MsgHandler(new MessageHandler(*this)), TFS(TFS), SupportedSymbolKinds(defaultSymbolKinds()), SupportedCompletionItemKinds(defaultCompletionItemKinds()), Opts(Opts) { // clang-format off MsgHandler->bind("initialize", &ClangdLSPServer::onInitialize); MsgHandler->bind("initialized", &ClangdLSPServer::onInitialized); MsgHandler->bind("shutdown", &ClangdLSPServer::onShutdown); MsgHandler->bind("sync", &ClangdLSPServer::onSync); MsgHandler->bind("textDocument/rangeFormatting", &ClangdLSPServer::onDocumentRangeFormatting); MsgHandler->bind("textDocument/onTypeFormatting", &ClangdLSPServer::onDocumentOnTypeFormatting); MsgHandler->bind("textDocument/formatting", &ClangdLSPServer::onDocumentFormatting); MsgHandler->bind("textDocument/codeAction", &ClangdLSPServer::onCodeAction); MsgHandler->bind("textDocument/completion", &ClangdLSPServer::onCompletion); MsgHandler->bind("textDocument/signatureHelp", &ClangdLSPServer::onSignatureHelp); MsgHandler->bind("textDocument/definition", &ClangdLSPServer::onGoToDefinition); MsgHandler->bind("textDocument/declaration", &ClangdLSPServer::onGoToDeclaration); MsgHandler->bind("textDocument/references", &ClangdLSPServer::onReference); MsgHandler->bind("textDocument/switchSourceHeader", &ClangdLSPServer::onSwitchSourceHeader); MsgHandler->bind("textDocument/prepareRename", &ClangdLSPServer::onPrepareRename); MsgHandler->bind("textDocument/rename", &ClangdLSPServer::onRename); MsgHandler->bind("textDocument/hover", &ClangdLSPServer::onHover); MsgHandler->bind("textDocument/documentSymbol", &ClangdLSPServer::onDocumentSymbol); MsgHandler->bind("workspace/executeCommand", &ClangdLSPServer::onCommand); MsgHandler->bind("textDocument/documentHighlight", &ClangdLSPServer::onDocumentHighlight); MsgHandler->bind("workspace/symbol", &ClangdLSPServer::onWorkspaceSymbol); MsgHandler->bind("textDocument/didOpen", &ClangdLSPServer::onDocumentDidOpen); MsgHandler->bind("textDocument/didClose", &ClangdLSPServer::onDocumentDidClose); MsgHandler->bind("textDocument/didChange", &ClangdLSPServer::onDocumentDidChange); MsgHandler->bind("textDocument/didSave", &ClangdLSPServer::onDocumentDidSave); MsgHandler->bind("workspace/didChangeWatchedFiles", &ClangdLSPServer::onFileEvent); MsgHandler->bind("workspace/didChangeConfiguration", &ClangdLSPServer::onChangeConfiguration); MsgHandler->bind("textDocument/symbolInfo", &ClangdLSPServer::onSymbolInfo); MsgHandler->bind("textDocument/typeHierarchy", &ClangdLSPServer::onTypeHierarchy); MsgHandler->bind("typeHierarchy/resolve", &ClangdLSPServer::onResolveTypeHierarchy); MsgHandler->bind("textDocument/selectionRange", &ClangdLSPServer::onSelectionRange); MsgHandler->bind("textDocument/documentLink", &ClangdLSPServer::onDocumentLink); MsgHandler->bind("textDocument/semanticTokens/full", &ClangdLSPServer::onSemanticTokens); MsgHandler->bind("textDocument/semanticTokens/full/delta", &ClangdLSPServer::onSemanticTokensDelta); if (Opts.FoldingRanges) MsgHandler->bind("textDocument/foldingRange", &ClangdLSPServer::onFoldingRange); // clang-format on } ClangdLSPServer::~ClangdLSPServer() { IsBeingDestroyed = true; // Explicitly destroy ClangdServer first, blocking on threads it owns. // This ensures they don't access any other members. Server.reset(); } bool ClangdLSPServer::run() { // Run the Language Server loop. bool CleanExit = true; if (auto Err = Transp.loop(*MsgHandler)) { elog("Transport error: {0}", std::move(Err)); CleanExit = false; } return CleanExit && ShutdownRequestReceived; } std::vector ClangdLSPServer::getFixes(llvm::StringRef File, const clangd::Diagnostic &D) { std::lock_guard Lock(FixItsMutex); auto DiagToFixItsIter = FixItsMap.find(File); if (DiagToFixItsIter == FixItsMap.end()) return {}; const auto &DiagToFixItsMap = DiagToFixItsIter->second; auto FixItsIter = DiagToFixItsMap.find(D); if (FixItsIter == DiagToFixItsMap.end()) return {}; return FixItsIter->second; } // A completion request is sent when the user types '>' or ':', but we only // want to trigger on '->' and '::'. We check the preceeding text to make // sure it matches what we expected. // Running the lexer here would be more robust (e.g. we can detect comments // and avoid triggering completion there), but we choose to err on the side // of simplicity here. bool ClangdLSPServer::shouldRunCompletion( const CompletionParams &Params) const { if (Params.context.triggerKind != CompletionTriggerKind::TriggerCharacter) return true; auto Code = DraftMgr.getDraft(Params.textDocument.uri.file()); if (!Code) return true; // completion code will log the error for untracked doc. auto Offset = positionToOffset(Code->Contents, Params.position, /*AllowColumnsBeyondLineLength=*/false); if (!Offset) { vlog("could not convert position '{0}' to offset for file '{1}'", Params.position, Params.textDocument.uri.file()); return true; } return allowImplicitCompletion(Code->Contents, *Offset); } void ClangdLSPServer::onHighlightingsReady( PathRef File, llvm::StringRef Version, std::vector Highlightings) { std::vector Old; std::vector HighlightingsCopy = Highlightings; { std::lock_guard Lock(HighlightingsMutex); Old = std::move(FileToHighlightings[File]); FileToHighlightings[File] = std::move(HighlightingsCopy); } // LSP allows us to send incremental edits of highlightings. Also need to diff // to remove highlightings from tokens that should no longer have them. std::vector Diffed = diffHighlightings(Highlightings, Old); TheiaSemanticHighlightingParams Notification; Notification.TextDocument.uri = URIForFile::canonicalize(File, /*TUPath=*/File); Notification.TextDocument.version = decodeVersion(Version); Notification.Lines = toTheiaSemanticHighlightingInformation(Diffed); publishTheiaSemanticHighlighting(Notification); } void ClangdLSPServer::onDiagnosticsReady(PathRef File, llvm::StringRef Version, std::vector Diagnostics) { PublishDiagnosticsParams Notification; Notification.version = decodeVersion(Version); Notification.uri = URIForFile::canonicalize(File, /*TUPath=*/File); DiagnosticToReplacementMap LocalFixIts; // Temporary storage for (auto &Diag : Diagnostics) { toLSPDiags(Diag, Notification.uri, DiagOpts, [&](clangd::Diagnostic Diag, llvm::ArrayRef Fixes) { auto &FixItsForDiagnostic = LocalFixIts[Diag]; llvm::copy(Fixes, std::back_inserter(FixItsForDiagnostic)); Notification.diagnostics.push_back(std::move(Diag)); }); } // Cache FixIts { std::lock_guard Lock(FixItsMutex); FixItsMap[File] = LocalFixIts; } // Send a notification to the LSP client. publishDiagnostics(Notification); } void ClangdLSPServer::onBackgroundIndexProgress( const BackgroundQueue::Stats &Stats) { static const char ProgressToken[] = "backgroundIndexProgress"; std::lock_guard Lock(BackgroundIndexProgressMutex); auto NotifyProgress = [this](const BackgroundQueue::Stats &Stats) { if (BackgroundIndexProgressState != BackgroundIndexProgress::Live) { WorkDoneProgressBegin Begin; Begin.percentage = true; Begin.title = "indexing"; progress(ProgressToken, std::move(Begin)); BackgroundIndexProgressState = BackgroundIndexProgress::Live; } if (Stats.Completed < Stats.Enqueued) { assert(Stats.Enqueued > Stats.LastIdle); WorkDoneProgressReport Report; Report.percentage = 100.0 * (Stats.Completed - Stats.LastIdle) / (Stats.Enqueued - Stats.LastIdle); Report.message = llvm::formatv("{0}/{1}", Stats.Completed - Stats.LastIdle, Stats.Enqueued - Stats.LastIdle); progress(ProgressToken, std::move(Report)); } else { assert(Stats.Completed == Stats.Enqueued); progress(ProgressToken, WorkDoneProgressEnd()); BackgroundIndexProgressState = BackgroundIndexProgress::Empty; } }; switch (BackgroundIndexProgressState) { case BackgroundIndexProgress::Unsupported: return; case BackgroundIndexProgress::Creating: // Cache this update for when the progress bar is available. PendingBackgroundIndexProgress = Stats; return; case BackgroundIndexProgress::Empty: { if (BackgroundIndexSkipCreate) { NotifyProgress(Stats); break; } // Cache this update for when the progress bar is available. PendingBackgroundIndexProgress = Stats; BackgroundIndexProgressState = BackgroundIndexProgress::Creating; WorkDoneProgressCreateParams CreateRequest; CreateRequest.token = ProgressToken; call( "window/workDoneProgress/create", CreateRequest, [this, NotifyProgress](llvm::Expected E) { std::lock_guard Lock(BackgroundIndexProgressMutex); if (E) { NotifyProgress(this->PendingBackgroundIndexProgress); } else { elog("Failed to create background index progress bar: {0}", E.takeError()); // give up forever rather than thrashing about BackgroundIndexProgressState = BackgroundIndexProgress::Unsupported; } }); break; } case BackgroundIndexProgress::Live: NotifyProgress(Stats); break; } } void ClangdLSPServer::onFileUpdated(PathRef File, const TUStatus &Status) { if (!SupportFileStatus) return; // FIXME: we don't emit "BuildingFile" and `RunningAction`, as these // two statuses are running faster in practice, which leads the UI constantly // changing, and doesn't provide much value. We may want to emit status at a // reasonable time interval (e.g. 0.5s). if (Status.PreambleActivity == PreambleAction::Idle && (Status.ASTActivity.K == ASTAction::Building || Status.ASTActivity.K == ASTAction::RunningAction)) return; notify("textDocument/clangd.fileStatus", Status.render(File)); } void ClangdLSPServer::reparseOpenFilesIfNeeded( llvm::function_ref Filter) { // Reparse only opened files that were modified. for (const Path &FilePath : DraftMgr.getActiveFiles()) if (Filter(FilePath)) if (auto Draft = DraftMgr.getDraft(FilePath)) // else disappeared in race? Server->addDocument(FilePath, std::move(Draft->Contents), encodeVersion(Draft->Version), WantDiagnostics::Auto); } } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp index 27d1a2dc7cdc..8c73b6a7d063 100644 --- a/clang-tools-extra/clangd/ClangdServer.cpp +++ b/clang-tools-extra/clangd/ClangdServer.cpp @@ -1,837 +1,837 @@ //===--- ClangdServer.cpp - Main clangd server code --------------*- C++-*-===// // // 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 "ClangdServer.h" #include "CodeComplete.h" #include "Config.h" #include "FindSymbols.h" #include "Format.h" #include "HeaderSourceSwitch.h" #include "Headers.h" #include "ParsedAST.h" #include "Preamble.h" #include "Protocol.h" #include "SemanticHighlighting.h" #include "SemanticSelection.h" #include "SourceCode.h" #include "TUScheduler.h" #include "XRefs.h" #include "index/CanonicalIncludes.h" #include "index/FileIndex.h" #include "index/Merge.h" #include "refactor/Rename.h" #include "refactor/Tweak.h" #include "support/Logger.h" #include "support/Markup.h" #include "support/ThreadsafeFS.h" #include "support/Trace.h" #include "clang/Format/Format.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/CompilerInvocation.h" #include "clang/Lex/Preprocessor.h" #include "clang/Tooling/CompilationDatabase.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/Optional.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Errc.h" #include "llvm/Support/Error.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/Path.h" #include "llvm/Support/ScopedPrinter.h" #include "llvm/Support/raw_ostream.h" #include #include #include #include #include #include #include namespace clang { namespace clangd { namespace { // Update the FileIndex with new ASTs and plumb the diagnostics responses. struct UpdateIndexCallbacks : public ParsingCallbacks { UpdateIndexCallbacks(FileIndex *FIndex, ClangdServer::Callbacks *ServerCallbacks, bool TheiaSemanticHighlighting) : FIndex(FIndex), ServerCallbacks(ServerCallbacks), TheiaSemanticHighlighting(TheiaSemanticHighlighting) {} void onPreambleAST(PathRef Path, llvm::StringRef Version, ASTContext &Ctx, std::shared_ptr PP, const CanonicalIncludes &CanonIncludes) override { if (FIndex) FIndex->updatePreamble(Path, Version, Ctx, std::move(PP), CanonIncludes); } void onMainAST(PathRef Path, ParsedAST &AST, PublishFn Publish) override { if (FIndex) FIndex->updateMain(Path, AST); std::vector Diagnostics = AST.getDiagnostics(); std::vector Highlightings; if (TheiaSemanticHighlighting) Highlightings = getSemanticHighlightings(AST); if (ServerCallbacks) Publish([&]() { ServerCallbacks->onDiagnosticsReady(Path, AST.version(), std::move(Diagnostics)); if (TheiaSemanticHighlighting) ServerCallbacks->onHighlightingsReady(Path, AST.version(), std::move(Highlightings)); }); } void onFailedAST(PathRef Path, llvm::StringRef Version, std::vector Diags, PublishFn Publish) override { if (ServerCallbacks) Publish( [&]() { ServerCallbacks->onDiagnosticsReady(Path, Version, Diags); }); } void onFileUpdated(PathRef File, const TUStatus &Status) override { if (ServerCallbacks) ServerCallbacks->onFileUpdated(File, Status); } private: FileIndex *FIndex; ClangdServer::Callbacks *ServerCallbacks; bool TheiaSemanticHighlighting; }; // Set of clang-tidy checks that don't work well in clangd, either due to // crashes or false positives. // Returns a clang-tidy filter string: -check1,-check2. llvm::StringRef getUnusableTidyChecks() { static const std::string FalsePositives = llvm::join_items(", ", // Check relies on seeing ifndef/define/endif directives, // clangd doesn't replay those when using a preamble. "-llvm-header-guard"); static const std::string CrashingChecks = llvm::join_items(", ", // Check can choke on invalid (intermediate) c++ code, // which is often the case when clangd tries to build an // AST. "-bugprone-use-after-move"); static const std::string UnusableTidyChecks = llvm::join_items(", ", FalsePositives, CrashingChecks); return UnusableTidyChecks; } // Returns a clang-tidy options string: check1,check2. llvm::StringRef getDefaultTidyChecks() { // These default checks are chosen for: // - low false-positive rate // - providing a lot of value // - being reasonably efficient static const std::string DefaultChecks = llvm::join_items( ",", "readability-misleading-indentation", "readability-deleted-default", "bugprone-integer-division", "bugprone-sizeof-expression", "bugprone-suspicious-missing-comma", "bugprone-unused-raii", "bugprone-unused-return-value", "misc-unused-using-decls", "misc-unused-alias-decls", "misc-definitions-in-headers"); return DefaultChecks; } } // namespace ClangdServer::Options ClangdServer::optsForTest() { ClangdServer::Options Opts; Opts.UpdateDebounce = DebouncePolicy::fixed(/*zero*/ {}); Opts.StorePreamblesInMemory = true; Opts.AsyncThreadsCount = 4; // Consistent! Opts.TheiaSemanticHighlighting = true; Opts.AsyncPreambleBuilds = true; return Opts; } ClangdServer::Options::operator TUScheduler::Options() const { TUScheduler::Options Opts; Opts.AsyncThreadsCount = AsyncThreadsCount; Opts.RetentionPolicy = RetentionPolicy; Opts.StorePreamblesInMemory = StorePreamblesInMemory; Opts.UpdateDebounce = UpdateDebounce; Opts.AsyncPreambleBuilds = AsyncPreambleBuilds; return Opts; } ClangdServer::ClangdServer(const GlobalCompilationDatabase &CDB, const ThreadsafeFS &TFS, const Options &Opts, Callbacks *Callbacks) : ConfigProvider(Opts.ConfigProvider), TFS(TFS), DynamicIdx(Opts.BuildDynamicSymbolIndex ? new FileIndex(Opts.HeavyweightDynamicSymbolIndex, Opts.CollectMainFileRefs) : nullptr), GetClangTidyOptions(Opts.GetClangTidyOptions), SuggestMissingIncludes(Opts.SuggestMissingIncludes), BuildRecoveryAST(Opts.BuildRecoveryAST), PreserveRecoveryASTType(Opts.PreserveRecoveryASTType), TweakFilter(Opts.TweakFilter), WorkspaceRoot(Opts.WorkspaceRoot), // Pass a callback into `WorkScheduler` to extract symbols from a newly // parsed file and rebuild the file index synchronously each time an AST // is parsed. // FIXME(ioeric): this can be slow and we may be able to index on less // critical paths. WorkScheduler( CDB, [&, this] { TUScheduler::Options O(Opts); O.ContextProvider = [this](PathRef P) { return createProcessingContext(P); }; return O; }(), std::make_unique( DynamicIdx.get(), Callbacks, Opts.TheiaSemanticHighlighting)) { // Adds an index to the stack, at higher priority than existing indexes. auto AddIndex = [&](SymbolIndex *Idx) { if (this->Index != nullptr) { MergedIdx.push_back(std::make_unique(Idx, this->Index)); this->Index = MergedIdx.back().get(); } else { this->Index = Idx; } }; if (Opts.StaticIndex) AddIndex(Opts.StaticIndex); if (Opts.BackgroundIndex) { BackgroundIndex::Options BGOpts; BGOpts.ThreadPoolSize = std::max(Opts.AsyncThreadsCount, 1u); BGOpts.OnProgress = [Callbacks](BackgroundQueue::Stats S) { if (Callbacks) Callbacks->onBackgroundIndexProgress(S); }; BGOpts.ContextProvider = [this](PathRef P) { return createProcessingContext(P); }; BackgroundIdx = std::make_unique( TFS, CDB, BackgroundIndexStorage::createDiskBackedStorageFactory( [&CDB](llvm::StringRef File) { return CDB.getProjectInfo(File); }), std::move(BGOpts)); AddIndex(BackgroundIdx.get()); } if (DynamicIdx) AddIndex(DynamicIdx.get()); } void ClangdServer::addDocument(PathRef File, llvm::StringRef Contents, llvm::StringRef Version, WantDiagnostics WantDiags, bool ForceRebuild) { ParseOptions Opts; Opts.ClangTidyOpts = tidy::ClangTidyOptions::getDefaults(); // FIXME: call tidy options builder on the worker thread, it can do IO. if (GetClangTidyOptions) Opts.ClangTidyOpts = GetClangTidyOptions(*TFS.view(/*CWD=*/llvm::None), File); if (Opts.ClangTidyOpts.Checks.hasValue()) { // If the set of checks was configured, make sure clangd incompatible ones // are disabled. Opts.ClangTidyOpts.Checks = llvm::join_items( ", ", *Opts.ClangTidyOpts.Checks, getUnusableTidyChecks()); } else { // Otherwise provide a nice set of defaults. Opts.ClangTidyOpts.Checks = getDefaultTidyChecks().str(); } Opts.SuggestMissingIncludes = SuggestMissingIncludes; // Compile command is set asynchronously during update, as it can be slow. ParseInputs Inputs; Inputs.TFS = &TFS; Inputs.Contents = std::string(Contents); Inputs.Version = Version.str(); Inputs.ForceRebuild = ForceRebuild; Inputs.Opts = std::move(Opts); Inputs.Index = Index; Inputs.Opts.BuildRecoveryAST = BuildRecoveryAST; Inputs.Opts.PreserveRecoveryASTType = PreserveRecoveryASTType; bool NewFile = WorkScheduler.update(File, Inputs, WantDiags); // If we loaded Foo.h, we want to make sure Foo.cpp is indexed. if (NewFile && BackgroundIdx) BackgroundIdx->boostRelated(File); } void ClangdServer::removeDocument(PathRef File) { WorkScheduler.remove(File); } void ClangdServer::codeComplete(PathRef File, Position Pos, const clangd::CodeCompleteOptions &Opts, Callback CB) { // Copy completion options for passing them to async task handler. auto CodeCompleteOpts = Opts; if (!CodeCompleteOpts.Index) // Respect overridden index. CodeCompleteOpts.Index = Index; auto Task = [Pos, CodeCompleteOpts, File = File.str(), CB = std::move(CB), this](llvm::Expected IP) mutable { if (!IP) return CB(IP.takeError()); if (auto Reason = isCancelled()) return CB(llvm::make_error(Reason)); llvm::Optional SpecFuzzyFind; if (!IP->Preamble) { // No speculation in Fallback mode, as it's supposed to be much faster // without compiling. vlog("Build for file {0} is not ready. Enter fallback mode.", File); } else { if (CodeCompleteOpts.Index && CodeCompleteOpts.SpeculativeIndexRequest) { SpecFuzzyFind.emplace(); { std::lock_guard Lock( CachedCompletionFuzzyFindRequestMutex); SpecFuzzyFind->CachedReq = CachedCompletionFuzzyFindRequestByFile[File]; } } } ParseInputs ParseInput{IP->Command, &TFS, IP->Contents.str()}; ParseInput.Index = Index; ParseInput.Opts.BuildRecoveryAST = BuildRecoveryAST; ParseInput.Opts.PreserveRecoveryASTType = PreserveRecoveryASTType; // FIXME(ibiryukov): even if Preamble is non-null, we may want to check // both the old and the new version in case only one of them matches. CodeCompleteResult Result = clangd::codeComplete( File, Pos, IP->Preamble, ParseInput, CodeCompleteOpts, SpecFuzzyFind ? SpecFuzzyFind.getPointer() : nullptr); { clang::clangd::trace::Span Tracer("Completion results callback"); CB(std::move(Result)); } if (SpecFuzzyFind && SpecFuzzyFind->NewReq.hasValue()) { std::lock_guard Lock(CachedCompletionFuzzyFindRequestMutex); CachedCompletionFuzzyFindRequestByFile[File] = SpecFuzzyFind->NewReq.getValue(); } // SpecFuzzyFind is only destroyed after speculative fuzzy find finishes. // We don't want `codeComplete` to wait for the async call if it doesn't use // the result (e.g. non-index completion, speculation fails), so that `CB` // is called as soon as results are available. }; // We use a potentially-stale preamble because latency is critical here. WorkScheduler.runWithPreamble( "CodeComplete", File, (Opts.RunParser == CodeCompleteOptions::AlwaysParse) ? TUScheduler::Stale : TUScheduler::StaleOrAbsent, std::move(Task)); } void ClangdServer::signatureHelp(PathRef File, Position Pos, Callback CB) { auto Action = [Pos, File = File.str(), CB = std::move(CB), this](llvm::Expected IP) mutable { if (!IP) return CB(IP.takeError()); const auto *PreambleData = IP->Preamble; if (!PreambleData) return CB(error("Failed to parse includes")); ParseInputs ParseInput{IP->Command, &TFS, IP->Contents.str()}; ParseInput.Index = Index; ParseInput.Opts.BuildRecoveryAST = BuildRecoveryAST; ParseInput.Opts.PreserveRecoveryASTType = PreserveRecoveryASTType; CB(clangd::signatureHelp(File, Pos, *PreambleData, ParseInput)); }; // Unlike code completion, we wait for a preamble here. WorkScheduler.runWithPreamble("SignatureHelp", File, TUScheduler::Stale, std::move(Action)); } void ClangdServer::formatRange(PathRef File, llvm::StringRef Code, Range Rng, Callback CB) { llvm::Expected Begin = positionToOffset(Code, Rng.start); if (!Begin) return CB(Begin.takeError()); llvm::Expected End = positionToOffset(Code, Rng.end); if (!End) return CB(End.takeError()); formatCode(File, Code, {tooling::Range(*Begin, *End - *Begin)}, std::move(CB)); } void ClangdServer::formatFile(PathRef File, llvm::StringRef Code, Callback CB) { // Format everything. formatCode(File, Code, {tooling::Range(0, Code.size())}, std::move(CB)); } void ClangdServer::formatOnType(PathRef File, llvm::StringRef Code, Position Pos, StringRef TriggerText, Callback> CB) { llvm::Expected CursorPos = positionToOffset(Code, Pos); if (!CursorPos) return CB(CursorPos.takeError()); auto Action = [File = File.str(), Code = Code.str(), TriggerText = TriggerText.str(), CursorPos = *CursorPos, CB = std::move(CB), this]() mutable { auto Style = format::getStyle(format::DefaultFormatStyle, File, format::DefaultFallbackStyle, Code, TFS.view(/*CWD=*/llvm::None).get()); if (!Style) return CB(Style.takeError()); std::vector Result; for (const tooling::Replacement &R : formatIncremental(Code, CursorPos, TriggerText, *Style)) Result.push_back(replacementToEdit(Code, R)); return CB(Result); }; WorkScheduler.run("FormatOnType", File, std::move(Action)); } void ClangdServer::prepareRename(PathRef File, Position Pos, const RenameOptions &RenameOpts, Callback> CB) { auto Action = [Pos, File = File.str(), CB = std::move(CB), RenameOpts, this](llvm::Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); auto &AST = InpAST->AST; const auto &SM = AST.getSourceManager(); auto Loc = sourceLocationInMainFile(SM, Pos); if (!Loc) return CB(Loc.takeError()); const auto *TouchingIdentifier = spelledIdentifierTouching(*Loc, AST.getTokens()); if (!TouchingIdentifier) return CB(llvm::None); // no rename on non-identifiers. auto Range = halfOpenToRange( SM, CharSourceRange::getCharRange(TouchingIdentifier->location(), TouchingIdentifier->endLocation())); if (RenameOpts.AllowCrossFile) // FIXME: we now assume cross-file rename always succeeds, revisit this. return CB(Range); // Performing the local rename isn't substantially more expensive than // doing an AST-based check, so we just rename and throw away the results. auto Changes = clangd::rename({Pos, "dummy", AST, File, Index, RenameOpts, /*GetDirtyBuffer=*/nullptr}); if (!Changes) { // LSP says to return null on failure, but that will result in a generic // failure message. If we send an LSP error response, clients can surface // the message to users (VSCode does). return CB(Changes.takeError()); } return CB(Range); }; WorkScheduler.runWithAST("PrepareRename", File, std::move(Action)); } void ClangdServer::rename(PathRef File, Position Pos, llvm::StringRef NewName, const RenameOptions &Opts, Callback CB) { // A snapshot of all file dirty buffers. llvm::StringMap Snapshot = WorkScheduler.getAllFileContents(); auto Action = [File = File.str(), NewName = NewName.str(), Pos, Opts, CB = std::move(CB), Snapshot = std::move(Snapshot), this](llvm::Expected InpAST) mutable { // Tracks number of files edited per invocation. static constexpr trace::Metric RenameFiles("rename_files", trace::Metric::Distribution); if (!InpAST) return CB(InpAST.takeError()); auto GetDirtyBuffer = [&Snapshot](PathRef AbsPath) -> llvm::Optional { auto It = Snapshot.find(AbsPath); if (It == Snapshot.end()) return llvm::None; return It->second; }; auto Edits = clangd::rename( {Pos, NewName, InpAST->AST, File, Index, Opts, GetDirtyBuffer}); if (!Edits) return CB(Edits.takeError()); if (Opts.WantFormat) { auto Style = getFormatStyleForFile(File, InpAST->Inputs.Contents, *InpAST->Inputs.TFS); llvm::Error Err = llvm::Error::success(); for (auto &E : *Edits) Err = llvm::joinErrors(reformatEdit(E.getValue(), Style), std::move(Err)); if (Err) return CB(std::move(Err)); } RenameFiles.record(Edits->size()); return CB(std::move(*Edits)); }; WorkScheduler.runWithAST("Rename", File, std::move(Action)); } // May generate several candidate selections, due to SelectionTree ambiguity. // vector of pointers because GCC doesn't like non-copyable Selection. static llvm::Expected>> tweakSelection(const Range &Sel, const InputsAndAST &AST) { auto Begin = positionToOffset(AST.Inputs.Contents, Sel.start); if (!Begin) return Begin.takeError(); auto End = positionToOffset(AST.Inputs.Contents, Sel.end); if (!End) return End.takeError(); std::vector> Result; SelectionTree::createEach( AST.AST.getASTContext(), AST.AST.getTokens(), *Begin, *End, [&](SelectionTree T) { Result.push_back(std::make_unique( AST.Inputs.Index, AST.AST, *Begin, *End, std::move(T))); return false; }); assert(!Result.empty() && "Expected at least one SelectionTree"); return std::move(Result); } void ClangdServer::enumerateTweaks(PathRef File, Range Sel, Callback> CB) { // Tracks number of times a tweak has been offered. static constexpr trace::Metric TweakAvailable( "tweak_available", trace::Metric::Counter, "tweak_id"); auto Action = [File = File.str(), Sel, CB = std::move(CB), this](Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); auto Selections = tweakSelection(Sel, *InpAST); if (!Selections) return CB(Selections.takeError()); std::vector Res; // Don't allow a tweak to fire more than once across ambiguous selections. llvm::DenseSet PreparedTweaks; auto Filter = [&](const Tweak &T) { return TweakFilter(T) && !PreparedTweaks.count(T.id()); }; for (const auto &Sel : *Selections) { for (auto &T : prepareTweaks(*Sel, Filter)) { - Res.push_back({T->id(), T->title(), T->intent()}); + Res.push_back({T->id(), T->title(), T->kind()}); PreparedTweaks.insert(T->id()); TweakAvailable.record(1, T->id()); } } CB(std::move(Res)); }; WorkScheduler.runWithAST("EnumerateTweaks", File, std::move(Action), TUScheduler::InvalidateOnUpdate); } void ClangdServer::applyTweak(PathRef File, Range Sel, StringRef TweakID, Callback CB) { // Tracks number of times a tweak has been attempted. static constexpr trace::Metric TweakAttempt( "tweak_attempt", trace::Metric::Counter, "tweak_id"); // Tracks number of times a tweak has failed to produce edits. static constexpr trace::Metric TweakFailed( "tweak_failed", trace::Metric::Counter, "tweak_id"); TweakAttempt.record(1, TweakID); auto Action = [File = File.str(), Sel, TweakID = TweakID.str(), CB = std::move(CB), this](Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); auto Selections = tweakSelection(Sel, *InpAST); if (!Selections) return CB(Selections.takeError()); llvm::Optional> Effect; // Try each selection, take the first one that prepare()s. // If they all fail, Effect will hold get the last error. for (const auto &Selection : *Selections) { auto T = prepareTweak(TweakID, *Selection); if (T) { Effect = (*T)->apply(*Selection); break; } Effect = T.takeError(); } assert(Effect.hasValue() && "Expected at least one selection"); if (*Effect) { // Tweaks don't apply clang-format, do that centrally here. for (auto &It : (*Effect)->ApplyEdits) { Edit &E = It.second; format::FormatStyle Style = getFormatStyleForFile(File, E.InitialCode, TFS); if (llvm::Error Err = reformatEdit(E, Style)) elog("Failed to format {0}: {1}", It.first(), std::move(Err)); } } else { TweakFailed.record(1, TweakID); } return CB(std::move(*Effect)); }; WorkScheduler.runWithAST("ApplyTweak", File, std::move(Action)); } void ClangdServer::locateSymbolAt(PathRef File, Position Pos, Callback> CB) { auto Action = [Pos, CB = std::move(CB), this](llvm::Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); CB(clangd::locateSymbolAt(InpAST->AST, Pos, Index)); }; WorkScheduler.runWithAST("Definitions", File, std::move(Action)); } void ClangdServer::switchSourceHeader( PathRef Path, Callback> CB) { // We want to return the result as fast as possible, strategy is: // 1) use the file-only heuristic, it requires some IO but it is much // faster than building AST, but it only works when .h/.cc files are in // the same directory. // 2) if 1) fails, we use the AST&Index approach, it is slower but supports // different code layout. if (auto CorrespondingFile = getCorrespondingHeaderOrSource( std::string(Path), TFS.view(llvm::None))) return CB(std::move(CorrespondingFile)); auto Action = [Path = Path.str(), CB = std::move(CB), this](llvm::Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); CB(getCorrespondingHeaderOrSource(Path, InpAST->AST, Index)); }; WorkScheduler.runWithAST("SwitchHeaderSource", Path, std::move(Action)); } void ClangdServer::formatCode(PathRef File, llvm::StringRef Code, llvm::ArrayRef Ranges, Callback CB) { // Call clang-format. auto Action = [File = File.str(), Code = Code.str(), Ranges = Ranges.vec(), CB = std::move(CB), this]() mutable { format::FormatStyle Style = getFormatStyleForFile(File, Code, TFS); tooling::Replacements IncludeReplaces = format::sortIncludes(Style, Code, Ranges, File); auto Changed = tooling::applyAllReplacements(Code, IncludeReplaces); if (!Changed) return CB(Changed.takeError()); CB(IncludeReplaces.merge(format::reformat( Style, *Changed, tooling::calculateRangesAfterReplacements(IncludeReplaces, Ranges), File))); }; WorkScheduler.run("Format", File, std::move(Action)); } void ClangdServer::findDocumentHighlights( PathRef File, Position Pos, Callback> CB) { auto Action = [Pos, CB = std::move(CB)](llvm::Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); CB(clangd::findDocumentHighlights(InpAST->AST, Pos)); }; WorkScheduler.runWithAST("Highlights", File, std::move(Action), TUScheduler::InvalidateOnUpdate); } void ClangdServer::findHover(PathRef File, Position Pos, Callback> CB) { auto Action = [File = File.str(), Pos, CB = std::move(CB), this](llvm::Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); format::FormatStyle Style = getFormatStyleForFile( File, InpAST->Inputs.Contents, *InpAST->Inputs.TFS); CB(clangd::getHover(InpAST->AST, Pos, std::move(Style), Index)); }; WorkScheduler.runWithAST("Hover", File, std::move(Action), TUScheduler::InvalidateOnUpdate); } void ClangdServer::typeHierarchy(PathRef File, Position Pos, int Resolve, TypeHierarchyDirection Direction, Callback> CB) { auto Action = [File = File.str(), Pos, Resolve, Direction, CB = std::move(CB), this](Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); CB(clangd::getTypeHierarchy(InpAST->AST, Pos, Resolve, Direction, Index, File)); }; WorkScheduler.runWithAST("Type Hierarchy", File, std::move(Action)); } void ClangdServer::resolveTypeHierarchy( TypeHierarchyItem Item, int Resolve, TypeHierarchyDirection Direction, Callback> CB) { clangd::resolveTypeHierarchy(Item, Resolve, Direction, Index); CB(Item); } void ClangdServer::onFileEvent(const DidChangeWatchedFilesParams &Params) { // FIXME: Do nothing for now. This will be used for indexing and potentially // invalidating other caches. } void ClangdServer::workspaceSymbols( llvm::StringRef Query, int Limit, Callback> CB) { WorkScheduler.run( "getWorkspaceSymbols", /*Path=*/"", [Query = Query.str(), Limit, CB = std::move(CB), this]() mutable { CB(clangd::getWorkspaceSymbols(Query, Limit, Index, WorkspaceRoot.getValueOr(""))); }); } void ClangdServer::documentSymbols(llvm::StringRef File, Callback> CB) { auto Action = [CB = std::move(CB)](llvm::Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); CB(clangd::getDocumentSymbols(InpAST->AST)); }; WorkScheduler.runWithAST("documentSymbols", File, std::move(Action), TUScheduler::InvalidateOnUpdate); } void ClangdServer::foldingRanges(llvm::StringRef File, Callback> CB) { auto Action = [CB = std::move(CB)](llvm::Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); CB(clangd::getFoldingRanges(InpAST->AST)); }; WorkScheduler.runWithAST("foldingRanges", File, std::move(Action), TUScheduler::InvalidateOnUpdate); } void ClangdServer::findReferences(PathRef File, Position Pos, uint32_t Limit, Callback CB) { auto Action = [Pos, Limit, CB = std::move(CB), this](llvm::Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); CB(clangd::findReferences(InpAST->AST, Pos, Limit, Index)); }; WorkScheduler.runWithAST("References", File, std::move(Action)); } void ClangdServer::symbolInfo(PathRef File, Position Pos, Callback> CB) { auto Action = [Pos, CB = std::move(CB)](llvm::Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); CB(clangd::getSymbolInfo(InpAST->AST, Pos)); }; WorkScheduler.runWithAST("SymbolInfo", File, std::move(Action)); } void ClangdServer::semanticRanges(PathRef File, const std::vector &Positions, Callback> CB) { auto Action = [Positions, CB = std::move(CB)]( llvm::Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); std::vector Result; for (const auto &Pos : Positions) { if (auto Range = clangd::getSemanticRanges(InpAST->AST, Pos)) Result.push_back(std::move(*Range)); else return CB(Range.takeError()); } CB(std::move(Result)); }; WorkScheduler.runWithAST("SemanticRanges", File, std::move(Action)); } void ClangdServer::documentLinks(PathRef File, Callback> CB) { auto Action = [CB = std::move(CB)](llvm::Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); CB(clangd::getDocumentLinks(InpAST->AST)); }; WorkScheduler.runWithAST("DocumentLinks", File, std::move(Action), TUScheduler::InvalidateOnUpdate); } void ClangdServer::semanticHighlights( PathRef File, Callback> CB) { auto Action = [CB = std::move(CB)](llvm::Expected InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); CB(clangd::getSemanticHighlightings(InpAST->AST)); }; WorkScheduler.runWithAST("SemanticHighlights", File, std::move(Action), TUScheduler::InvalidateOnUpdate); } void ClangdServer::customAction(PathRef File, llvm::StringRef Name, Callback Action) { WorkScheduler.runWithAST(Name, File, std::move(Action)); } llvm::StringMap ClangdServer::fileStats() const { return WorkScheduler.fileStats(); } Context ClangdServer::createProcessingContext(PathRef File) const { if (!ConfigProvider) return Context::current().clone(); config::Params Params; // Don't reread config files excessively often. // FIXME: when we see a config file change event, use the event timestamp. Params.FreshTime = std::chrono::steady_clock::now() - std::chrono::seconds(5); llvm::SmallString<256> PosixPath; if (!File.empty()) { assert(llvm::sys::path::is_absolute(File)); llvm::sys::path::native(File, PosixPath, llvm::sys::path::Style::posix); Params.Path = PosixPath.str(); } auto DiagnosticHandler = [](const llvm::SMDiagnostic &Diag) { if (Diag.getKind() == llvm::SourceMgr::DK_Error) { elog("config error at {0}:{1}:{2}: {3}", Diag.getFilename(), Diag.getLineNo(), Diag.getColumnNo(), Diag.getMessage()); } else { log("config warning at {0}:{1}:{2}: {3}", Diag.getFilename(), Diag.getLineNo(), Diag.getColumnNo(), Diag.getMessage()); } }; Config C = ConfigProvider->getConfig(Params, DiagnosticHandler); return Context::current().derive(Config::Key, std::move(C)); } LLVM_NODISCARD bool ClangdServer::blockUntilIdleForTest(llvm::Optional TimeoutSeconds) { return WorkScheduler.blockUntilIdle(timeoutSeconds(TimeoutSeconds)) && (!BackgroundIdx || BackgroundIdx->blockUntilIdleForTest(TimeoutSeconds)); } } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h index ae10dba32b58..d801d3cd4353 100644 --- a/clang-tools-extra/clangd/ClangdServer.h +++ b/clang-tools-extra/clangd/ClangdServer.h @@ -1,399 +1,399 @@ //===--- ClangdServer.h - Main clangd server code ----------------*- C++-*-===// // // 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 // //===----------------------------------------------------------------------===// #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_CLANGDSERVER_H #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_CLANGDSERVER_H #include "../clang-tidy/ClangTidyOptions.h" #include "CodeComplete.h" #include "ConfigProvider.h" #include "GlobalCompilationDatabase.h" #include "Hover.h" #include "Protocol.h" #include "SemanticHighlighting.h" #include "TUScheduler.h" #include "XRefs.h" #include "index/Background.h" #include "index/FileIndex.h" #include "index/Index.h" #include "refactor/Rename.h" #include "refactor/Tweak.h" #include "support/Cancellation.h" #include "support/Function.h" #include "support/ThreadsafeFS.h" #include "clang/Tooling/CompilationDatabase.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/FunctionExtras.h" #include "llvm/ADT/IntrusiveRefCntPtr.h" #include "llvm/ADT/Optional.h" #include "llvm/ADT/StringRef.h" #include #include #include #include #include namespace clang { namespace clangd { /// When set, used by ClangdServer to get clang-tidy options for each particular /// file. Must be thread-safe. We use this instead of ClangTidyOptionsProvider /// to allow reading tidy configs from the VFS used for parsing. using ClangTidyOptionsBuilder = std::function; /// Manages a collection of source files and derived data (ASTs, indexes), /// and provides language-aware features such as code completion. /// /// The primary client is ClangdLSPServer which exposes these features via /// the Language Server protocol. ClangdServer may also be embedded directly, /// though its API is not stable over time. /// /// ClangdServer should be used from a single thread. Many potentially-slow /// operations have asynchronous APIs and deliver their results on another /// thread. /// Such operations support cancellation: if the caller sets up a cancelable /// context, many operations will notice cancellation and fail early. /// (ClangdLSPServer uses this to implement $/cancelRequest). class ClangdServer { public: /// Interface with hooks for users of ClangdServer to be notified of events. class Callbacks { public: virtual ~Callbacks() = default; /// Called by ClangdServer when \p Diagnostics for \p File are ready. /// May be called concurrently for separate files, not for a single file. virtual void onDiagnosticsReady(PathRef File, llvm::StringRef Version, std::vector Diagnostics) {} /// Called whenever the file status is updated. /// May be called concurrently for separate files, not for a single file. virtual void onFileUpdated(PathRef File, const TUStatus &Status) {} /// Called by ClangdServer when some \p Highlightings for \p File are ready. /// May be called concurrently for separate files, not for a single file. virtual void onHighlightingsReady(PathRef File, llvm::StringRef Version, std::vector Highlightings) {} /// Called when background indexing tasks are enqueued/started/completed. /// Not called concurrently. virtual void onBackgroundIndexProgress(const BackgroundQueue::Stats &Stats) {} }; struct Options { /// To process requests asynchronously, ClangdServer spawns worker threads. /// If this is zero, no threads are spawned. All work is done on the calling /// thread, and callbacks are invoked before "async" functions return. unsigned AsyncThreadsCount = getDefaultAsyncThreadsCount(); /// AST caching policy. The default is to keep up to 3 ASTs in memory. ASTRetentionPolicy RetentionPolicy; /// Cached preambles are potentially large. If false, store them on disk. bool StorePreamblesInMemory = true; /// Reuse even stale preambles, and rebuild them in the background. /// This improves latency at the cost of accuracy. bool AsyncPreambleBuilds = true; /// If true, ClangdServer builds a dynamic in-memory index for symbols in /// opened files and uses the index to augment code completion results. bool BuildDynamicSymbolIndex = false; /// Use a heavier and faster in-memory index implementation. bool HeavyweightDynamicSymbolIndex = true; /// If true, ClangdServer automatically indexes files in the current project /// on background threads. The index is stored in the project root. bool BackgroundIndex = false; /// Store refs to main-file symbols in the index. bool CollectMainFileRefs = false; /// If set, use this index to augment code completion results. SymbolIndex *StaticIndex = nullptr; /// If set, queried to obtain the configuration to handle each request. config::Provider *ConfigProvider = nullptr; /// If set, enable clang-tidy in clangd and use to it get clang-tidy /// configurations for a particular file. /// Clangd supports only a small subset of ClangTidyOptions, these options /// (Checks, CheckOptions) are about which clang-tidy checks will be /// enabled. ClangTidyOptionsBuilder GetClangTidyOptions; /// If true, turn on the `-frecovery-ast` clang flag. bool BuildRecoveryAST = true; /// If true, turn on the `-frecovery-ast-type` clang flag. bool PreserveRecoveryASTType = true; /// Clangd's workspace root. Relevant for "workspace" operations not bound /// to a particular file. /// FIXME: If not set, should use the current working directory. llvm::Optional WorkspaceRoot; /// The resource directory is used to find internal headers, overriding /// defaults and -resource-dir compiler flag). /// If None, ClangdServer calls CompilerInvocation::GetResourcePath() to /// obtain the standard resource directory. llvm::Optional ResourceDir = llvm::None; /// Time to wait after a new file version before computing diagnostics. DebouncePolicy UpdateDebounce = DebouncePolicy{ /*Min=*/std::chrono::milliseconds(50), /*Max=*/std::chrono::milliseconds(500), /*RebuildRatio=*/1, }; bool SuggestMissingIncludes = false; /// Clangd will execute compiler drivers matching one of these globs to /// fetch system include path. std::vector QueryDriverGlobs; /// Enable notification-based semantic highlighting. bool TheiaSemanticHighlighting = false; /// Enable preview of FoldingRanges feature. bool FoldingRanges = false; /// Returns true if the tweak should be enabled. std::function TweakFilter = [](const Tweak &T) { return !T.hidden(); // only enable non-hidden tweaks. }; explicit operator TUScheduler::Options() const; }; // Sensible default options for use in tests. // Features like indexing must be enabled if desired. static Options optsForTest(); /// Creates a new ClangdServer instance. /// /// ClangdServer uses \p CDB to obtain compilation arguments for parsing. Note /// that ClangdServer only obtains compilation arguments once for each newly /// added file (i.e., when processing a first call to addDocument) and reuses /// those arguments for subsequent reparses. However, ClangdServer will check /// if compilation arguments changed on calls to forceReparse(). ClangdServer(const GlobalCompilationDatabase &CDB, const ThreadsafeFS &TFS, const Options &Opts, Callbacks *Callbacks = nullptr); /// Add a \p File to the list of tracked C++ files or update the contents if /// \p File is already tracked. Also schedules parsing of the AST for it on a /// separate thread. When the parsing is complete, DiagConsumer passed in /// constructor will receive onDiagnosticsReady callback. /// Version identifies this snapshot and is propagated to ASTs, preambles, /// diagnostics etc built from it. void addDocument(PathRef File, StringRef Contents, llvm::StringRef Version = "null", WantDiagnostics WD = WantDiagnostics::Auto, bool ForceRebuild = false); /// Remove \p File from list of tracked files, schedule a request to free /// resources associated with it. Pending diagnostics for closed files may not /// be delivered, even if requested with WantDiags::Auto or WantDiags::Yes. /// An empty set of diagnostics will be delivered, with Version = "". void removeDocument(PathRef File); /// Run code completion for \p File at \p Pos. /// Request is processed asynchronously. /// /// This method should only be called for currently tracked files. However, it /// is safe to call removeDocument for \p File after this method returns, even /// while returned future is not yet ready. /// A version of `codeComplete` that runs \p Callback on the processing thread /// when codeComplete results become available. void codeComplete(PathRef File, Position Pos, const clangd::CodeCompleteOptions &Opts, Callback CB); /// Provide signature help for \p File at \p Pos. This method should only be /// called for tracked files. void signatureHelp(PathRef File, Position Pos, Callback CB); /// Find declaration/definition locations of symbol at a specified position. void locateSymbolAt(PathRef File, Position Pos, Callback> CB); /// Switch to a corresponding source file when given a header file, and vice /// versa. void switchSourceHeader(PathRef Path, Callback> CB); /// Get document highlights for a given position. void findDocumentHighlights(PathRef File, Position Pos, Callback> CB); /// Get code hover for a given position. void findHover(PathRef File, Position Pos, Callback> CB); /// Get information about type hierarchy for a given position. void typeHierarchy(PathRef File, Position Pos, int Resolve, TypeHierarchyDirection Direction, Callback> CB); /// Resolve type hierarchy item in the given direction. void resolveTypeHierarchy(TypeHierarchyItem Item, int Resolve, TypeHierarchyDirection Direction, Callback> CB); /// Retrieve the top symbols from the workspace matching a query. void workspaceSymbols(StringRef Query, int Limit, Callback> CB); /// Retrieve the symbols within the specified file. void documentSymbols(StringRef File, Callback> CB); /// Retrieve ranges that can be used to fold code within the specified file. void foldingRanges(StringRef File, Callback> CB); /// Retrieve locations for symbol references. void findReferences(PathRef File, Position Pos, uint32_t Limit, Callback CB); /// Run formatting for \p Rng inside \p File with content \p Code. void formatRange(PathRef File, StringRef Code, Range Rng, Callback CB); /// Run formatting for the whole \p File with content \p Code. void formatFile(PathRef File, StringRef Code, Callback CB); /// Run formatting after \p TriggerText was typed at \p Pos in \p File with /// content \p Code. void formatOnType(PathRef File, StringRef Code, Position Pos, StringRef TriggerText, Callback> CB); /// Test the validity of a rename operation. void prepareRename(PathRef File, Position Pos, const RenameOptions &RenameOpts, Callback> CB); /// Rename all occurrences of the symbol at the \p Pos in \p File to /// \p NewName. /// If WantFormat is false, the final TextEdit will be not formatted, /// embedders could use this method to get all occurrences of the symbol (e.g. /// highlighting them in prepare stage). void rename(PathRef File, Position Pos, llvm::StringRef NewName, const RenameOptions &Opts, Callback CB); struct TweakRef { std::string ID; /// ID to pass for applyTweak. std::string Title; /// A single-line message to show in the UI. - Tweak::Intent Intent; + llvm::StringLiteral Kind; }; /// Enumerate the code tweaks available to the user at a specified point. void enumerateTweaks(PathRef File, Range Sel, Callback> CB); /// Apply the code tweak with a specified \p ID. void applyTweak(PathRef File, Range Sel, StringRef ID, Callback CB); /// Called when an event occurs for a watched file in the workspace. void onFileEvent(const DidChangeWatchedFilesParams &Params); /// Get symbol info for given position. /// Clangd extension - not part of official LSP. void symbolInfo(PathRef File, Position Pos, Callback> CB); /// Get semantic ranges around a specified position in a file. void semanticRanges(PathRef File, const std::vector &Pos, Callback> CB); /// Get all document links in a file. void documentLinks(PathRef File, Callback> CB); void semanticHighlights(PathRef File, Callback>); /// Runs an arbitrary action that has access to the AST of the specified file. /// The action will execute on one of ClangdServer's internal threads. /// The AST is only valid for the duration of the callback. /// As with other actions, the file must have been opened. void customAction(PathRef File, llvm::StringRef Name, Callback Action); /// Returns estimated memory usage and other statistics for each of the /// currently open files. /// Overall memory usage of clangd may be significantly more than reported /// here, as this metric does not account (at least) for: /// - memory occupied by static and dynamic index, /// - memory required for in-flight requests, /// FIXME: those metrics might be useful too, we should add them. llvm::StringMap fileStats() const; // Blocks the main thread until the server is idle. Only for use in tests. // Returns false if the timeout expires. LLVM_NODISCARD bool blockUntilIdleForTest(llvm::Optional TimeoutSeconds = 10); private: void formatCode(PathRef File, llvm::StringRef Code, ArrayRef Ranges, Callback CB); /// Derives a context for a task processing the specified source file. /// This includes the current configuration (see Options::ConfigProvider). /// The empty string means no particular file is the target. /// Rather than called by each feature, this is exposed to the components /// that control worker threads, like TUScheduler and BackgroundIndex. /// This means it's OK to do some IO here, and it cuts across all features. Context createProcessingContext(PathRef) const; config::Provider *ConfigProvider = nullptr; const ThreadsafeFS &TFS; Path ResourceDir; // The index used to look up symbols. This could be: // - null (all index functionality is optional) // - the dynamic index owned by ClangdServer (DynamicIdx) // - the static index passed to the constructor // - a merged view of a static and dynamic index (MergedIndex) const SymbolIndex *Index = nullptr; // If present, an index of symbols in open files. Read via *Index. std::unique_ptr DynamicIdx; // If present, the new "auto-index" maintained in background threads. std::unique_ptr BackgroundIdx; // Storage for merged views of the various indexes. std::vector> MergedIdx; // When set, provides clang-tidy options for a specific file. ClangTidyOptionsBuilder GetClangTidyOptions; // If this is true, suggest include insertion fixes for diagnostic errors that // can be caused by missing includes (e.g. member access in incomplete type). bool SuggestMissingIncludes = false; // If true, preserve expressions in AST for broken code. bool BuildRecoveryAST = true; // If true, preserve the type for recovery AST. bool PreserveRecoveryASTType = false; std::function TweakFilter; // GUARDED_BY(CachedCompletionFuzzyFindRequestMutex) llvm::StringMap> CachedCompletionFuzzyFindRequestByFile; mutable std::mutex CachedCompletionFuzzyFindRequestMutex; llvm::Optional WorkspaceRoot; // WorkScheduler has to be the last member, because its destructor has to be // called before all other members to stop the worker thread that references // ClangdServer. TUScheduler WorkScheduler; }; } // namespace clangd } // namespace clang #endif diff --git a/clang-tools-extra/clangd/refactor/Tweak.h b/clang-tools-extra/clangd/refactor/Tweak.h index 10e3e8d3e565..f991b78d8960 100644 --- a/clang-tools-extra/clangd/refactor/Tweak.h +++ b/clang-tools-extra/clangd/refactor/Tweak.h @@ -1,148 +1,141 @@ //===--- Tweak.h -------------------------------------------------*- C++-*-===// // // 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 // //===----------------------------------------------------------------------===// // Tweaks are small actions that run over the AST and produce edits, messages // etc as a result. They are local, i.e. they should take the current editor // context, e.g. the cursor position and selection into account. // The actions are executed in two stages: // - Stage 1 should check whether the action is available in a current // context. It should be cheap and fast to compute as it is executed for all // available actions on every client request, which happen quite frequently. // - Stage 2 is performed after stage 1 and can be more expensive to compute. // It is performed when the user actually chooses the action. //===----------------------------------------------------------------------===// #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_REFACTOR_ACTIONS_TWEAK_H #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_REFACTOR_ACTIONS_TWEAK_H #include "ParsedAST.h" #include "Protocol.h" #include "Selection.h" #include "SourceCode.h" #include "index/Index.h" #include "support/Path.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/Optional.h" #include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Error.h" #include namespace clang { namespace clangd { /// An interface base for small context-sensitive refactoring actions. /// To implement a new tweak use the following pattern in a .cpp file: /// class MyTweak : public Tweak { /// public: /// const char* id() const override final; // defined by REGISTER_TWEAK. /// // implement other methods here. /// }; /// REGISTER_TWEAK(MyTweak); class Tweak { public: /// Input to prepare and apply tweaks. struct Selection { Selection(const SymbolIndex *Index, ParsedAST &AST, unsigned RangeBegin, unsigned RangeEnd, SelectionTree ASTSelection); /// The text of the active document. llvm::StringRef Code; /// The Index for handling codebase related queries. const SymbolIndex *Index = nullptr; /// The parsed active file. Never null. (Pointer so Selection is movable). ParsedAST *AST; /// A location of the cursor in the editor. // FIXME: Cursor is redundant and should be removed SourceLocation Cursor; /// The begin offset of the selection unsigned SelectionBegin; /// The end offset of the selection unsigned SelectionEnd; /// The AST nodes that were selected. SelectionTree ASTSelection; // FIXME: provide a way to get sources and ASTs for other files. }; - /// Output of a tweak. - enum Intent { - /// Apply changes that preserve the behavior of the code. - Refactor, - /// Provide information to the user. - Info, - }; struct Effect { /// A message to be displayed to the user. llvm::Optional ShowMessage; FileEdits ApplyEdits; static Effect showMessage(StringRef S) { Effect E; E.ShowMessage = std::string(S); return E; } /// Path is the absolute, symlink-resolved path for the file pointed by FID /// in SM. Edit is generated from Replacements. /// Fails if cannot figure out absolute path for FID. static llvm::Expected> fileEdit(const SourceManager &SM, FileID FID, tooling::Replacements Replacements); /// Creates an effect with an Edit for the main file. /// Fails if cannot figure out absolute path for main file. static llvm::Expected mainFileEdit(const SourceManager &SM, tooling::Replacements Replacements); }; virtual ~Tweak() = default; /// A unique id of the action, it is always equal to the name of the class /// defining the Tweak. Definition is provided automatically by /// REGISTER_TWEAK. virtual const char *id() const = 0; /// Run the first stage of the action. Returns true indicating that the /// action is available and should be shown to the user. Returns false if the /// action is not available. /// This function should be fast, if the action requires non-trivial work it /// should be moved into 'apply'. /// Returns true iff the action is available and apply() can be called on it. virtual bool prepare(const Selection &Sel) = 0; /// Run the second stage of the action that would produce the actual effect. /// EXPECTS: prepare() was called and returned true. virtual Expected apply(const Selection &Sel) = 0; /// A one-line title of the action that should be shown to the users in the /// UI. /// EXPECTS: prepare() was called and returned true. virtual std::string title() const = 0; /// Describes what kind of action this is. /// EXPECTS: prepare() was called and returned true. - virtual Intent intent() const = 0; + virtual llvm::StringLiteral kind() const = 0; /// Is this a 'hidden' tweak, which are off by default. virtual bool hidden() const { return false; } }; // All tweaks must be registered in the .cpp file next to their definition. #define REGISTER_TWEAK(Subclass) \ ::llvm::Registry<::clang::clangd::Tweak>::Add \ TweakRegistrationFor##Subclass(#Subclass, /*Description=*/""); \ const char *Subclass::id() const { return #Subclass; } /// Calls prepare() on all tweaks that satisfy the filter, returning those that /// can run on the selection. std::vector> prepareTweaks(const Tweak::Selection &S, llvm::function_ref Filter); // Calls prepare() on the tweak with a given ID. // If prepare() returns false, returns an error. // If prepare() returns true, returns the corresponding tweak. llvm::Expected> prepareTweak(StringRef TweakID, const Tweak::Selection &S); } // namespace clangd } // namespace clang #endif diff --git a/clang-tools-extra/clangd/refactor/tweaks/AddUsing.cpp b/clang-tools-extra/clangd/refactor/tweaks/AddUsing.cpp index 8c69b64c5aff..fe01894b6386 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/AddUsing.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/AddUsing.cpp @@ -1,331 +1,333 @@ //===--- AddUsing.cpp --------------------------------------------*- C++-*-===// // // 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 "AST.h" #include "Config.h" #include "FindTarget.h" #include "refactor/Tweak.h" #include "support/Logger.h" #include "clang/AST/Decl.h" #include "clang/AST/RecursiveASTVisitor.h" namespace clang { namespace clangd { namespace { // Tweak for removing full namespace qualifier under cursor on DeclRefExpr and // types and adding "using" statement instead. // // Only qualifiers that refer exclusively to namespaces (no record types) are // supported. There is some guessing of appropriate place to insert the using // declaration. If we find any existing usings, we insert it there. If not, we // insert right after the inner-most relevant namespace declaration. If there is // none, or there is, but it was declared via macro, we insert above the first // top level decl. // // Currently this only removes qualifier from under the cursor. In the future, // we should improve this to remove qualifier from all occurrences of this // symbol. class AddUsing : public Tweak { public: const char *id() const override; bool prepare(const Selection &Inputs) override; Expected apply(const Selection &Inputs) override; std::string title() const override; - Intent intent() const override { return Refactor; } + llvm::StringLiteral kind() const override { + return CodeAction::REFACTOR_KIND; + } private: // The qualifier to remove. Set by prepare(). NestedNameSpecifierLoc QualifierToRemove; // The name following QualifierToRemove. Set by prepare(). llvm::StringRef Name; }; REGISTER_TWEAK(AddUsing) std::string AddUsing::title() const { return std::string(llvm::formatv( "Add using-declaration for {0} and remove qualifier.", Name)); } // Locates all "using" statements relevant to SelectionDeclContext. class UsingFinder : public RecursiveASTVisitor { public: UsingFinder(std::vector &Results, const DeclContext *SelectionDeclContext, const SourceManager &SM) : Results(Results), SelectionDeclContext(SelectionDeclContext), SM(SM) {} bool VisitUsingDecl(UsingDecl *D) { auto Loc = D->getUsingLoc(); if (SM.getFileID(Loc) != SM.getMainFileID()) { return true; } if (D->getDeclContext()->Encloses(SelectionDeclContext)) { Results.push_back(D); } return true; } bool TraverseDecl(Decl *Node) { // There is no need to go deeper into nodes that do not enclose selection, // since "using" there will not affect selection, nor would it make a good // insertion point. if (Node->getDeclContext()->Encloses(SelectionDeclContext)) { return RecursiveASTVisitor::TraverseDecl(Node); } return true; } private: std::vector &Results; const DeclContext *SelectionDeclContext; const SourceManager &SM; }; bool isFullyQualified(const NestedNameSpecifier *NNS) { if (!NNS) return false; return NNS->getKind() == NestedNameSpecifier::Global || isFullyQualified(NNS->getPrefix()); } struct InsertionPointData { // Location to insert the "using" statement. If invalid then the statement // should not be inserted at all (it already exists). SourceLocation Loc; // Extra suffix to place after the "using" statement. Depending on what the // insertion point is anchored to, we may need one or more \n to ensure // proper formatting. std::string Suffix; // Whether using should be fully qualified, even if what the user typed was // not. This is based on our detection of the local style. bool AlwaysFullyQualify = false; }; // Finds the best place to insert the "using" statement. Returns invalid // SourceLocation if the "using" statement already exists. // // The insertion point might be a little awkward if the decl we're anchoring to // has a comment in an unfortunate place (e.g. directly above function or using // decl, or immediately following "namespace {". We should add some helpers for // dealing with that and use them in other code modifications as well. llvm::Expected findInsertionPoint(const Tweak::Selection &Inputs, const NestedNameSpecifierLoc &QualifierToRemove, const llvm::StringRef Name) { auto &SM = Inputs.AST->getSourceManager(); // Search for all using decls that affect this point in file. We need this for // two reasons: to skip adding "using" if one already exists and to find best // place to add it, if it doesn't exist. SourceLocation LastUsingLoc; std::vector Usings; UsingFinder(Usings, &Inputs.ASTSelection.commonAncestor()->getDeclContext(), SM) .TraverseAST(Inputs.AST->getASTContext()); bool AlwaysFullyQualify = true; for (auto &U : Usings) { // Only "upgrade" to fully qualified is all relevant using decls are fully // qualified. Otherwise trust what the user typed. if (!isFullyQualified(U->getQualifier())) AlwaysFullyQualify = false; if (SM.isBeforeInTranslationUnit(Inputs.Cursor, U->getUsingLoc())) // "Usings" is sorted, so we're done. break; if (U->getQualifier()->getAsNamespace()->getCanonicalDecl() == QualifierToRemove.getNestedNameSpecifier() ->getAsNamespace() ->getCanonicalDecl() && U->getName() == Name) { return InsertionPointData(); } // Insertion point will be before last UsingDecl that affects cursor // position. For most cases this should stick with the local convention of // add using inside or outside namespace. LastUsingLoc = U->getUsingLoc(); } if (LastUsingLoc.isValid()) { InsertionPointData Out; Out.Loc = LastUsingLoc; Out.AlwaysFullyQualify = AlwaysFullyQualify; return Out; } // No relevant "using" statements. Try the nearest namespace level. const DeclContext *ParentDeclCtx = &Inputs.ASTSelection.commonAncestor()->getDeclContext(); while (ParentDeclCtx && !ParentDeclCtx->isFileContext()) { ParentDeclCtx = ParentDeclCtx->getLexicalParent(); } if (auto *ND = llvm::dyn_cast_or_null(ParentDeclCtx)) { auto Toks = Inputs.AST->getTokens().expandedTokens(ND->getSourceRange()); const auto *Tok = llvm::find_if(Toks, [](const syntax::Token &Tok) { return Tok.kind() == tok::l_brace; }); if (Tok == Toks.end() || Tok->endLocation().isInvalid()) { return error("Namespace with no {"); } if (!Tok->endLocation().isMacroID()) { InsertionPointData Out; Out.Loc = Tok->endLocation(); Out.Suffix = "\n"; return Out; } } // No using, no namespace, no idea where to insert. Try above the first // top level decl. auto TLDs = Inputs.AST->getLocalTopLevelDecls(); if (TLDs.empty()) { return error("Cannot find place to insert \"using\""); } InsertionPointData Out; Out.Loc = SM.getExpansionLoc(TLDs[0]->getBeginLoc()); Out.Suffix = "\n\n"; return Out; } bool isNamespaceForbidden(const Tweak::Selection &Inputs, const NestedNameSpecifier &Namespace) { std::string NamespaceStr = printNamespaceScope(*Namespace.getAsNamespace()); for (StringRef Banned : Config::current().Style.FullyQualifiedNamespaces) { StringRef PrefixMatch = NamespaceStr; if (PrefixMatch.consume_front(Banned) && PrefixMatch.consume_front("::")) return true; } return false; } bool AddUsing::prepare(const Selection &Inputs) { auto &SM = Inputs.AST->getSourceManager(); // Do not suggest "using" in header files. That way madness lies. if (isHeaderFile(SM.getFileEntryForID(SM.getMainFileID())->getName(), Inputs.AST->getLangOpts())) return false; auto *Node = Inputs.ASTSelection.commonAncestor(); if (Node == nullptr) return false; // If we're looking at a type or NestedNameSpecifier, walk up the tree until // we find the "main" node we care about, which would be ElaboratedTypeLoc or // DeclRefExpr. for (; Node->Parent; Node = Node->Parent) { if (Node->ASTNode.get()) { continue; } else if (auto *T = Node->ASTNode.get()) { if (T->getAs()) { break; } else if (Node->Parent->ASTNode.get() || Node->Parent->ASTNode.get()) { // Node is TypeLoc, but it's parent is either TypeLoc or // NestedNameSpecifier. In both cases, we want to go up, to find // the outermost TypeLoc. continue; } } break; } if (Node == nullptr) return false; if (auto *D = Node->ASTNode.get()) { if (auto *II = D->getDecl()->getIdentifier()) { QualifierToRemove = D->getQualifierLoc(); Name = II->getName(); } } else if (auto *T = Node->ASTNode.get()) { if (auto E = T->getAs()) { if (auto *BaseTypeIdentifier = E.getType().getUnqualifiedType().getBaseTypeIdentifier()) { Name = BaseTypeIdentifier->getName(); QualifierToRemove = E.getQualifierLoc(); } } } // FIXME: This only supports removing qualifiers that are made up of just // namespace names. If qualifier contains a type, we could take the longest // namespace prefix and remove that. if (!QualifierToRemove.hasQualifier() || !QualifierToRemove.getNestedNameSpecifier()->getAsNamespace() || Name.empty()) { return false; } if (isNamespaceForbidden(Inputs, *QualifierToRemove.getNestedNameSpecifier())) return false; // Macros are difficult. We only want to offer code action when what's spelled // under the cursor is a namespace qualifier. If it's a macro that expands to // a qualifier, user would not know what code action will actually change. // On the other hand, if the qualifier is part of the macro argument, we // should still support that. if (SM.isMacroBodyExpansion(QualifierToRemove.getBeginLoc()) || !SM.isWrittenInSameFile(QualifierToRemove.getBeginLoc(), QualifierToRemove.getEndLoc())) { return false; } return true; } Expected AddUsing::apply(const Selection &Inputs) { auto &SM = Inputs.AST->getSourceManager(); auto &TB = Inputs.AST->getTokens(); // Determine the length of the qualifier under the cursor, then remove it. auto SpelledTokens = TB.spelledForExpanded( TB.expandedTokens(QualifierToRemove.getSourceRange())); if (!SpelledTokens) { return error("Could not determine length of the qualifier"); } unsigned Length = syntax::Token::range(SM, SpelledTokens->front(), SpelledTokens->back()) .length(); tooling::Replacements R; if (auto Err = R.add(tooling::Replacement( SM, SpelledTokens->front().location(), Length, ""))) { return std::move(Err); } auto InsertionPoint = findInsertionPoint(Inputs, QualifierToRemove, Name); if (!InsertionPoint) { return InsertionPoint.takeError(); } if (InsertionPoint->Loc.isValid()) { // Add the using statement at appropriate location. std::string UsingText; llvm::raw_string_ostream UsingTextStream(UsingText); UsingTextStream << "using "; if (InsertionPoint->AlwaysFullyQualify && !isFullyQualified(QualifierToRemove.getNestedNameSpecifier())) UsingTextStream << "::"; QualifierToRemove.getNestedNameSpecifier()->print( UsingTextStream, Inputs.AST->getASTContext().getPrintingPolicy()); UsingTextStream << Name << ";" << InsertionPoint->Suffix; assert(SM.getFileID(InsertionPoint->Loc) == SM.getMainFileID()); if (auto Err = R.add(tooling::Replacement(SM, InsertionPoint->Loc, 0, UsingTextStream.str()))) { return std::move(Err); } } return Effect::mainFileEdit(Inputs.AST->getASTContext().getSourceManager(), std::move(R)); } } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/refactor/tweaks/AnnotateHighlightings.cpp b/clang-tools-extra/clangd/refactor/tweaks/AnnotateHighlightings.cpp index 8e3eba35b004..b243f24eb369 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/AnnotateHighlightings.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/AnnotateHighlightings.cpp @@ -1,79 +1,81 @@ //===--- AnnotateHighlightings.cpp -------------------------------*- C++-*-===// // // 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 "SemanticHighlighting.h" #include "refactor/Tweak.h" #include "llvm/ADT/StringRef.h" namespace clang { namespace clangd { namespace { /// Annotate all highlighting tokens in the current file. This is a hidden tweak /// which is used to debug semantic highlightings. /// Before: /// void f() { int abc; } /// ^^^^^^^^^^^^^^^^^^^^^ /// After: /// void /* entity.name.function.cpp */ f() { int /* variable.cpp */ abc; } class AnnotateHighlightings : public Tweak { public: const char *id() const override final; bool prepare(const Selection &Inputs) override { return true; } Expected apply(const Selection &Inputs) override; std::string title() const override { return "Annotate highlighting tokens"; } - Intent intent() const override { return Refactor; } + llvm::StringLiteral kind() const override { + return CodeAction::REFACTOR_KIND; + } bool hidden() const override { return true; } }; REGISTER_TWEAK(AnnotateHighlightings) Expected AnnotateHighlightings::apply(const Selection &Inputs) { const Decl *CommonDecl = nullptr; for (auto N = Inputs.ASTSelection.commonAncestor(); N && !CommonDecl; N = N->Parent) CommonDecl = N->ASTNode.get(); std::vector HighlightingTokens; if (!CommonDecl) { // Now we hit the TUDecl case where commonAncestor() returns null // intendedly. We only annotate tokens in the main file, so use the default // traversal scope (which is the top level decls of the main file). HighlightingTokens = getSemanticHighlightings(*Inputs.AST); } else { // Store the existing scopes. const auto &BackupScopes = Inputs.AST->getASTContext().getTraversalScope(); // Narrow the traversal scope to the selected node. Inputs.AST->getASTContext().setTraversalScope( {const_cast(CommonDecl)}); HighlightingTokens = getSemanticHighlightings(*Inputs.AST); // Restore the traversal scope. Inputs.AST->getASTContext().setTraversalScope(BackupScopes); } auto &SM = Inputs.AST->getSourceManager(); tooling::Replacements Result; llvm::StringRef FilePath = SM.getFilename(Inputs.Cursor); for (const auto &Token : HighlightingTokens) { assert(Token.R.start.line == Token.R.end.line && "Token must be at the same line"); auto InsertOffset = positionToOffset(Inputs.Code, Token.R.start); if (!InsertOffset) return InsertOffset.takeError(); auto InsertReplacement = tooling::Replacement( FilePath, *InsertOffset, 0, ("/* " + toTextMateScope(Token.Kind) + " */").str()); if (auto Err = Result.add(InsertReplacement)) return std::move(Err); } return Effect::mainFileEdit(SM, std::move(Result)); } } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/refactor/tweaks/DefineInline.cpp b/clang-tools-extra/clangd/refactor/tweaks/DefineInline.cpp index cdd5f9c6595b..02be220e0b6c 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/DefineInline.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/DefineInline.cpp @@ -1,519 +1,521 @@ //===--- DefineInline.cpp ----------------------------------------*- C++-*-===// // // 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 "AST.h" #include "FindTarget.h" #include "Selection.h" #include "SourceCode.h" #include "XRefs.h" #include "refactor/Tweak.h" #include "support/Logger.h" #include "clang/AST/ASTContext.h" #include "clang/AST/ASTTypeTraits.h" #include "clang/AST/Decl.h" #include "clang/AST/DeclBase.h" #include "clang/AST/DeclCXX.h" #include "clang/AST/DeclTemplate.h" #include "clang/AST/Expr.h" #include "clang/AST/ExprCXX.h" #include "clang/AST/NestedNameSpecifier.h" #include "clang/AST/PrettyPrinter.h" #include "clang/AST/RecursiveASTVisitor.h" #include "clang/AST/Stmt.h" #include "clang/AST/TemplateBase.h" #include "clang/AST/Type.h" #include "clang/AST/TypeLoc.h" #include "clang/Basic/LangOptions.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" #include "clang/Basic/TokenKinds.h" #include "clang/Driver/Types.h" #include "clang/Index/IndexDataConsumer.h" #include "clang/Index/IndexSymbol.h" #include "clang/Index/IndexingAction.h" #include "clang/Lex/Lexer.h" #include "clang/Lex/Preprocessor.h" #include "clang/Lex/Token.h" #include "clang/Sema/Lookup.h" #include "clang/Sema/Sema.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/DenseSet.h" #include "llvm/ADT/None.h" #include "llvm/ADT/Optional.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Casting.h" #include "llvm/Support/Error.h" #include "llvm/Support/FormatAdapters.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/Signals.h" #include "llvm/Support/raw_ostream.h" #include #include #include #include #include #include namespace clang { namespace clangd { namespace { // Returns semicolon location for the given FD. Since AST doesn't contain that // information, searches for a semicolon by lexing from end of function decl // while skipping comments. llvm::Optional getSemicolonForDecl(const FunctionDecl *FD) { const SourceManager &SM = FD->getASTContext().getSourceManager(); const LangOptions &LangOpts = FD->getASTContext().getLangOpts(); SourceLocation CurLoc = FD->getEndLoc(); auto NextTok = Lexer::findNextToken(CurLoc, SM, LangOpts); if (!NextTok || !NextTok->is(tok::semi)) return llvm::None; return NextTok->getLocation(); } // Deduces the FunctionDecl from a selection. Requires either the function body // or the function decl to be selected. Returns null if none of the above // criteria is met. const FunctionDecl *getSelectedFunction(const SelectionTree::Node *SelNode) { const ast_type_traits::DynTypedNode &AstNode = SelNode->ASTNode; if (const FunctionDecl *FD = AstNode.get()) return FD; if (AstNode.get() && SelNode->Selected == SelectionTree::Complete) { if (const SelectionTree::Node *P = SelNode->Parent) return P->ASTNode.get(); } return nullptr; } // Checks the decls mentioned in Source are visible in the context of Target. // Achieves that by checking declarations occur before target location in // translation unit or declared in the same class. bool checkDeclsAreVisible(const llvm::DenseSet &DeclRefs, const FunctionDecl *Target, const SourceManager &SM) { SourceLocation TargetLoc = Target->getLocation(); // To be used in visibility check below, decls in a class are visible // independent of order. const RecordDecl *Class = nullptr; if (const auto *MD = llvm::dyn_cast(Target)) Class = MD->getParent(); for (const auto *DR : DeclRefs) { // Use canonical decl, since having one decl before target is enough. const Decl *D = DR->getCanonicalDecl(); if (D == Target) continue; SourceLocation DeclLoc = D->getLocation(); // FIXME: Allow declarations from different files with include insertion. if (!SM.isWrittenInSameFile(DeclLoc, TargetLoc)) return false; // If declaration is before target, then it is visible. if (SM.isBeforeInTranslationUnit(DeclLoc, TargetLoc)) continue; // Otherwise they need to be in same class if (!Class) return false; const RecordDecl *Parent = nullptr; if (const auto *MD = llvm::dyn_cast(D)) Parent = MD->getParent(); else if (const auto *FD = llvm::dyn_cast(D)) Parent = FD->getParent(); if (Parent != Class) return false; } return true; } // Rewrites body of FD by re-spelling all of the names to make sure they are // still valid in context of Target. llvm::Expected qualifyAllDecls(const FunctionDecl *FD, const FunctionDecl *Target) { // There are three types of spellings that needs to be qualified in a function // body: // - Types: Foo -> ns::Foo // - DeclRefExpr: ns2::foo() -> ns1::ns2::foo(); // - UsingDecls: // using ns2::foo -> using ns1::ns2::foo // using namespace ns2 -> using namespace ns1::ns2 // using ns3 = ns2 -> using ns3 = ns1::ns2 // // Go over all references inside a function body to generate replacements that // will qualify those. So that body can be moved into an arbitrary file. // We perform the qualification by qualifying the first type/decl in a // (un)qualified name. e.g: // namespace a { namespace b { class Bar{}; void foo(); } } // b::Bar x; -> a::b::Bar x; // foo(); -> a::b::foo(); auto *TargetContext = Target->getLexicalDeclContext(); const SourceManager &SM = FD->getASTContext().getSourceManager(); tooling::Replacements Replacements; bool HadErrors = false; findExplicitReferences(FD->getBody(), [&](ReferenceLoc Ref) { // Since we want to qualify only the first qualifier, skip names with a // qualifier. if (Ref.Qualifier) return; // There might be no decl in dependent contexts, there's nothing much we can // do in such cases. if (Ref.Targets.empty()) return; // Do not qualify names introduced by macro expansions. if (Ref.NameLoc.isMacroID()) return; for (const NamedDecl *ND : Ref.Targets) { if (ND->getDeclContext() != Ref.Targets.front()->getDeclContext()) { elog("define inline: Targets from multiple contexts: {0}, {1}", printQualifiedName(*Ref.Targets.front()), printQualifiedName(*ND)); HadErrors = true; return; } } // All Targets are in the same scope, so we can safely chose first one. const NamedDecl *ND = Ref.Targets.front(); // Skip anything from a non-namespace scope, these can be: // - Function or Method scopes, which means decl is local and doesn't need // qualification. // - From Class/Struct/Union scope, which again doesn't need any qualifiers, // rather the left side of it requires qualification, like: // namespace a { class Bar { public: static int x; } } // void foo() { Bar::x; } // ~~~~~ -> we need to qualify Bar not x. if (!ND->getDeclContext()->isNamespace()) return; const std::string Qualifier = getQualification( FD->getASTContext(), TargetContext, Target->getBeginLoc(), ND); if (auto Err = Replacements.add( tooling::Replacement(SM, Ref.NameLoc, 0, Qualifier))) { HadErrors = true; elog("define inline: Failed to add quals: {0}", std::move(Err)); } }); if (HadErrors) return error( "define inline: Failed to compute qualifiers. See logs for details."); // Get new begin and end positions for the qualified body. auto OrigBodyRange = toHalfOpenFileRange( SM, FD->getASTContext().getLangOpts(), FD->getBody()->getSourceRange()); if (!OrigBodyRange) return error("Couldn't get range func body."); unsigned BodyBegin = SM.getFileOffset(OrigBodyRange->getBegin()); unsigned BodyEnd = Replacements.getShiftedCodePosition( SM.getFileOffset(OrigBodyRange->getEnd())); // Trim the result to function body. auto QualifiedFunc = tooling::applyAllReplacements( SM.getBufferData(SM.getFileID(OrigBodyRange->getBegin())), Replacements); if (!QualifiedFunc) return QualifiedFunc.takeError(); return QualifiedFunc->substr(BodyBegin, BodyEnd - BodyBegin + 1); } /// Generates Replacements for changing template and function parameter names in /// \p Dest to be the same as in \p Source. llvm::Expected renameParameters(const FunctionDecl *Dest, const FunctionDecl *Source) { llvm::DenseMap ParamToNewName; llvm::DenseMap> RefLocs; auto HandleParam = [&](const NamedDecl *DestParam, const NamedDecl *SourceParam) { // No need to rename if parameters already have the same name. if (DestParam->getName() == SourceParam->getName()) return; std::string NewName; // Unnamed parameters won't be visited in findExplicitReferences. So add // them here. if (DestParam->getName().empty()) { RefLocs[DestParam].push_back(DestParam->getLocation()); // If decl is unnamed in destination we pad the new name to avoid gluing // with previous token, e.g. foo(int^) shouldn't turn into foo(intx). NewName = " "; } NewName.append(std::string(SourceParam->getName())); ParamToNewName[DestParam->getCanonicalDecl()] = std::move(NewName); }; // Populate mapping for template parameters. auto *DestTempl = Dest->getDescribedFunctionTemplate(); auto *SourceTempl = Source->getDescribedFunctionTemplate(); assert(bool(DestTempl) == bool(SourceTempl)); if (DestTempl) { const auto *DestTPL = DestTempl->getTemplateParameters(); const auto *SourceTPL = SourceTempl->getTemplateParameters(); assert(DestTPL->size() == SourceTPL->size()); for (size_t I = 0, EP = DestTPL->size(); I != EP; ++I) HandleParam(DestTPL->getParam(I), SourceTPL->getParam(I)); } // Populate mapping for function params. assert(Dest->param_size() == Source->param_size()); for (size_t I = 0, E = Dest->param_size(); I != E; ++I) HandleParam(Dest->getParamDecl(I), Source->getParamDecl(I)); const SourceManager &SM = Dest->getASTContext().getSourceManager(); const LangOptions &LangOpts = Dest->getASTContext().getLangOpts(); // Collect other references in function signature, i.e parameter types and // default arguments. findExplicitReferences( // Use function template in case of templated functions to visit template // parameters. DestTempl ? llvm::dyn_cast(DestTempl) : llvm::dyn_cast(Dest), [&](ReferenceLoc Ref) { if (Ref.Targets.size() != 1) return; const auto *Target = llvm::cast(Ref.Targets.front()->getCanonicalDecl()); auto It = ParamToNewName.find(Target); if (It == ParamToNewName.end()) return; RefLocs[Target].push_back(Ref.NameLoc); }); // Now try to generate edits for all the refs. tooling::Replacements Replacements; for (auto &Entry : RefLocs) { const auto *OldDecl = Entry.first; llvm::StringRef OldName = OldDecl->getName(); llvm::StringRef NewName = ParamToNewName[OldDecl]; for (SourceLocation RefLoc : Entry.second) { CharSourceRange ReplaceRange; // In case of unnamed parameters, we have an empty char range, whereas we // have a tokenrange at RefLoc with named parameters. if (OldName.empty()) ReplaceRange = CharSourceRange::getCharRange(RefLoc, RefLoc); else ReplaceRange = CharSourceRange::getTokenRange(RefLoc, RefLoc); // If occurrence is coming from a macro expansion, try to get back to the // file range. if (RefLoc.isMacroID()) { ReplaceRange = Lexer::makeFileCharRange(ReplaceRange, SM, LangOpts); // Bail out if we need to replace macro bodies. if (ReplaceRange.isInvalid()) { auto Err = error("Cant rename parameter inside macro body."); elog("define inline: {0}", Err); return std::move(Err); } } if (auto Err = Replacements.add( tooling::Replacement(SM, ReplaceRange, NewName))) { elog("define inline: Couldn't replace parameter name for {0} to {1}: " "{2}", OldName, NewName, Err); return std::move(Err); } } } return Replacements; } // Returns the canonical declaration for the given FunctionDecl. This will // usually be the first declaration in current translation unit with the // exception of template specialization. // For those we return first declaration different than the canonical one. // Because canonical declaration points to template decl instead of // specialization. const FunctionDecl *findTarget(const FunctionDecl *FD) { auto CanonDecl = FD->getCanonicalDecl(); if (!FD->isFunctionTemplateSpecialization() || CanonDecl == FD) return CanonDecl; // For specializations CanonicalDecl is the TemplatedDecl, which is not the // target we want to inline into. Instead we traverse previous decls to find // the first forward decl for this specialization. auto PrevDecl = FD; while (PrevDecl->getPreviousDecl() != CanonDecl) { PrevDecl = PrevDecl->getPreviousDecl(); assert(PrevDecl && "Found specialization without template decl"); } return PrevDecl; } // Returns the beginning location for a FunctionDecl. Returns location of // template keyword for templated functions. const SourceLocation getBeginLoc(const FunctionDecl *FD) { // Include template parameter list. if (auto *FTD = FD->getDescribedFunctionTemplate()) return FTD->getBeginLoc(); return FD->getBeginLoc(); } llvm::Optional addInlineIfInHeader(const FunctionDecl *FD) { // This includes inline functions and constexpr functions. if (FD->isInlined() || llvm::isa(FD)) return llvm::None; // Primary template doesn't need inline. if (FD->isTemplated() && !FD->isFunctionTemplateSpecialization()) return llvm::None; const SourceManager &SM = FD->getASTContext().getSourceManager(); llvm::StringRef FileName = SM.getFilename(FD->getLocation()); // If it is not a header we don't need to mark function as "inline". if (!isHeaderFile(FileName, FD->getASTContext().getLangOpts())) return llvm::None; return tooling::Replacement(SM, FD->getInnerLocStart(), 0, "inline "); } /// Moves definition of a function/method to its declaration location. /// Before: /// a.h: /// void foo(); /// /// a.cc: /// void foo() { return; } /// /// ------------------------ /// After: /// a.h: /// void foo() { return; } /// /// a.cc: /// class DefineInline : public Tweak { public: const char *id() const override final; - Intent intent() const override { return Intent::Refactor; } + llvm::StringLiteral kind() const override { + return CodeAction::REFACTOR_KIND; + } std::string title() const override { return "Move function body to declaration"; } // Returns true when selection is on a function definition that does not // make use of any internal symbols. bool prepare(const Selection &Sel) override { const SelectionTree::Node *SelNode = Sel.ASTSelection.commonAncestor(); if (!SelNode) return false; Source = getSelectedFunction(SelNode); if (!Source || !Source->hasBody()) return false; // Only the last level of template parameter locations are not kept in AST, // so if we are inlining a method that is in a templated class, there is no // way to verify template parameter names. Therefore we bail out. if (auto *MD = llvm::dyn_cast(Source)) { if (MD->getParent()->isTemplated()) return false; } // If function body starts or ends inside a macro, we refuse to move it into // declaration location. if (Source->getBody()->getBeginLoc().isMacroID() || Source->getBody()->getEndLoc().isMacroID()) return false; Target = findTarget(Source); if (Target == Source) { // The only declaration is Source. No other declaration to move function // body. // FIXME: If we are in an implementation file, figure out a suitable // location to put declaration. Possibly using other declarations in the // AST. return false; } // Check if the decls referenced in function body are visible in the // declaration location. if (!checkDeclsAreVisible(getNonLocalDeclRefs(*Sel.AST, Source), Target, Sel.AST->getSourceManager())) return false; return true; } Expected apply(const Selection &Sel) override { const auto &AST = Sel.AST->getASTContext(); const auto &SM = AST.getSourceManager(); auto Semicolon = getSemicolonForDecl(Target); if (!Semicolon) return error("Couldn't find semicolon for target declaration."); auto AddInlineIfNecessary = addInlineIfInHeader(Target); auto ParamReplacements = renameParameters(Target, Source); if (!ParamReplacements) return ParamReplacements.takeError(); auto QualifiedBody = qualifyAllDecls(Source, Target); if (!QualifiedBody) return QualifiedBody.takeError(); const tooling::Replacement SemicolonToFuncBody(SM, *Semicolon, 1, *QualifiedBody); tooling::Replacements TargetFileReplacements(SemicolonToFuncBody); TargetFileReplacements = TargetFileReplacements.merge(*ParamReplacements); if (AddInlineIfNecessary) { if (auto Err = TargetFileReplacements.add(*AddInlineIfNecessary)) return std::move(Err); } auto DefRange = toHalfOpenFileRange( SM, AST.getLangOpts(), SM.getExpansionRange(CharSourceRange::getCharRange(getBeginLoc(Source), Source->getEndLoc())) .getAsRange()); if (!DefRange) return error("Couldn't get range for the source."); unsigned int SourceLen = SM.getFileOffset(DefRange->getEnd()) - SM.getFileOffset(DefRange->getBegin()); const tooling::Replacement DeleteFuncBody(SM, DefRange->getBegin(), SourceLen, ""); llvm::SmallVector, 2> Edits; // Edit for Target. auto FE = Effect::fileEdit(SM, SM.getFileID(*Semicolon), std::move(TargetFileReplacements)); if (!FE) return FE.takeError(); Edits.push_back(std::move(*FE)); // Edit for Source. if (!SM.isWrittenInSameFile(DefRange->getBegin(), SM.getExpansionLoc(Target->getBeginLoc()))) { // Generate a new edit if the Source and Target are in different files. auto FE = Effect::fileEdit(SM, SM.getFileID(Sel.Cursor), tooling::Replacements(DeleteFuncBody)); if (!FE) return FE.takeError(); Edits.push_back(std::move(*FE)); } else { // Merge with previous edit if they are in the same file. if (auto Err = Edits.front().second.Replacements.add(DeleteFuncBody)) return std::move(Err); } Effect E; for (auto &Pair : Edits) E.ApplyEdits.try_emplace(std::move(Pair.first), std::move(Pair.second)); return E; } private: const FunctionDecl *Source = nullptr; const FunctionDecl *Target = nullptr; }; REGISTER_TWEAK(DefineInline) } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/refactor/tweaks/DefineOutline.cpp b/clang-tools-extra/clangd/refactor/tweaks/DefineOutline.cpp index ed4d0cc46269..0462090ee25a 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/DefineOutline.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/DefineOutline.cpp @@ -1,460 +1,462 @@ //===--- DefineOutline.cpp ---------------------------------------*- C++-*-===// // // 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 "AST.h" #include "FindTarget.h" #include "HeaderSourceSwitch.h" #include "ParsedAST.h" #include "Selection.h" #include "SourceCode.h" #include "refactor/Tweak.h" #include "support/Logger.h" #include "support/Path.h" #include "clang/AST/ASTTypeTraits.h" #include "clang/AST/Attr.h" #include "clang/AST/Decl.h" #include "clang/AST/DeclBase.h" #include "clang/AST/DeclCXX.h" #include "clang/AST/DeclTemplate.h" #include "clang/AST/Stmt.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" #include "clang/Basic/TokenKinds.h" #include "clang/Driver/Types.h" #include "clang/Format/Format.h" #include "clang/Lex/Lexer.h" #include "clang/Tooling/Core/Replacement.h" #include "clang/Tooling/Syntax/Tokens.h" #include "llvm/ADT/None.h" #include "llvm/ADT/Optional.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Casting.h" #include "llvm/Support/Error.h" #include #include namespace clang { namespace clangd { namespace { // Deduces the FunctionDecl from a selection. Requires either the function body // or the function decl to be selected. Returns null if none of the above // criteria is met. // FIXME: This is shared with define inline, move them to a common header once // we have a place for such. const FunctionDecl *getSelectedFunction(const SelectionTree::Node *SelNode) { if (!SelNode) return nullptr; const ast_type_traits::DynTypedNode &AstNode = SelNode->ASTNode; if (const FunctionDecl *FD = AstNode.get()) return FD; if (AstNode.get() && SelNode->Selected == SelectionTree::Complete) { if (const SelectionTree::Node *P = SelNode->Parent) return P->ASTNode.get(); } return nullptr; } llvm::Optional getSourceFile(llvm::StringRef FileName, const Tweak::Selection &Sel) { if (auto Source = getCorrespondingHeaderOrSource( std::string(FileName), &Sel.AST->getSourceManager().getFileManager().getVirtualFileSystem())) return *Source; return getCorrespondingHeaderOrSource(std::string(FileName), *Sel.AST, Sel.Index); } // Synthesize a DeclContext for TargetNS from CurContext. TargetNS must be empty // for global namespace, and endwith "::" otherwise. // Returns None if TargetNS is not a prefix of CurContext. llvm::Optional findContextForNS(llvm::StringRef TargetNS, const DeclContext *CurContext) { assert(TargetNS.empty() || TargetNS.endswith("::")); // Skip any non-namespace contexts, e.g. TagDecls, functions/methods. CurContext = CurContext->getEnclosingNamespaceContext(); // If TargetNS is empty, it means global ns, which is translation unit. if (TargetNS.empty()) { while (!CurContext->isTranslationUnit()) CurContext = CurContext->getParent(); return CurContext; } // Otherwise we need to drop any trailing namespaces from CurContext until // we reach TargetNS. std::string TargetContextNS = CurContext->isNamespace() ? llvm::cast(CurContext)->getQualifiedNameAsString() : ""; TargetContextNS.append("::"); llvm::StringRef CurrentContextNS(TargetContextNS); // If TargetNS is not a prefix of CurrentContext, there's no way to reach // it. if (!CurrentContextNS.startswith(TargetNS)) return llvm::None; while (CurrentContextNS != TargetNS) { CurContext = CurContext->getParent(); // These colons always exists since TargetNS is a prefix of // CurrentContextNS, it ends with "::" and they are not equal. CurrentContextNS = CurrentContextNS.take_front( CurrentContextNS.drop_back(2).rfind("::") + 2); } return CurContext; } // Returns source code for FD after applying Replacements. // FIXME: Make the function take a parameter to return only the function body, // afterwards it can be shared with define-inline code action. llvm::Expected getFunctionSourceAfterReplacements(const FunctionDecl *FD, const tooling::Replacements &Replacements) { const auto &SM = FD->getASTContext().getSourceManager(); auto OrigFuncRange = toHalfOpenFileRange( SM, FD->getASTContext().getLangOpts(), FD->getSourceRange()); if (!OrigFuncRange) return error("Couldn't get range for function."); assert(!FD->getDescribedFunctionTemplate() && "Define out-of-line doesn't apply to function templates."); // Get new begin and end positions for the qualified function definition. unsigned FuncBegin = SM.getFileOffset(OrigFuncRange->getBegin()); unsigned FuncEnd = Replacements.getShiftedCodePosition( SM.getFileOffset(OrigFuncRange->getEnd())); // Trim the result to function definition. auto QualifiedFunc = tooling::applyAllReplacements( SM.getBufferData(SM.getMainFileID()), Replacements); if (!QualifiedFunc) return QualifiedFunc.takeError(); return QualifiedFunc->substr(FuncBegin, FuncEnd - FuncBegin + 1); } // Creates a modified version of function definition that can be inserted at a // different location, qualifies return value and function name to achieve that. // Contains function signature, except defaulted parameter arguments, body and // template parameters if applicable. No need to qualify parameters, as they are // looked up in the context containing the function/method. // FIXME: Drop attributes in function signature. llvm::Expected getFunctionSourceCode(const FunctionDecl *FD, llvm::StringRef TargetNamespace, const syntax::TokenBuffer &TokBuf) { auto &AST = FD->getASTContext(); auto &SM = AST.getSourceManager(); auto TargetContext = findContextForNS(TargetNamespace, FD->getDeclContext()); if (!TargetContext) return error("define outline: couldn't find a context for target"); llvm::Error Errors = llvm::Error::success(); tooling::Replacements DeclarationCleanups; // Finds the first unqualified name in function return type and name, then // qualifies those to be valid in TargetContext. findExplicitReferences(FD, [&](ReferenceLoc Ref) { // It is enough to qualify the first qualifier, so skip references with a // qualifier. Also we can't do much if there are no targets or name is // inside a macro body. if (Ref.Qualifier || Ref.Targets.empty() || Ref.NameLoc.isMacroID()) return; // Only qualify return type and function name. if (Ref.NameLoc != FD->getReturnTypeSourceRange().getBegin() && Ref.NameLoc != FD->getLocation()) return; for (const NamedDecl *ND : Ref.Targets) { if (ND->getDeclContext() != Ref.Targets.front()->getDeclContext()) { elog("Targets from multiple contexts: {0}, {1}", printQualifiedName(*Ref.Targets.front()), printQualifiedName(*ND)); return; } } const NamedDecl *ND = Ref.Targets.front(); const std::string Qualifier = getQualification( AST, *TargetContext, SM.getLocForStartOfFile(SM.getMainFileID()), ND); if (auto Err = DeclarationCleanups.add( tooling::Replacement(SM, Ref.NameLoc, 0, Qualifier))) Errors = llvm::joinErrors(std::move(Errors), std::move(Err)); }); // Get rid of default arguments, since they should not be specified in // out-of-line definition. for (const auto *PVD : FD->parameters()) { if (PVD->hasDefaultArg()) { // Deletion range initially spans the initializer, excluding the `=`. auto DelRange = CharSourceRange::getTokenRange(PVD->getDefaultArgRange()); // Get all tokens before the default argument. auto Tokens = TokBuf.expandedTokens(PVD->getSourceRange()) .take_while([&SM, &DelRange](const syntax::Token &Tok) { return SM.isBeforeInTranslationUnit( Tok.location(), DelRange.getBegin()); }); // Find the last `=` before the default arg. auto Tok = llvm::find_if(llvm::reverse(Tokens), [](const syntax::Token &Tok) { return Tok.kind() == tok::equal; }); assert(Tok != Tokens.rend()); DelRange.setBegin(Tok->location()); if (auto Err = DeclarationCleanups.add(tooling::Replacement(SM, DelRange, ""))) Errors = llvm::joinErrors(std::move(Errors), std::move(Err)); } } auto DelAttr = [&](const Attr *A) { if (!A) return; auto AttrTokens = TokBuf.spelledForExpanded(TokBuf.expandedTokens(A->getRange())); assert(A->getLocation().isValid()); if (!AttrTokens || AttrTokens->empty()) { Errors = llvm::joinErrors( std::move(Errors), error("define outline: Can't move out of line as " "function has a macro `{0}` specifier.", A->getSpelling())); return; } CharSourceRange DelRange = syntax::Token::range(SM, AttrTokens->front(), AttrTokens->back()) .toCharRange(SM); if (auto Err = DeclarationCleanups.add(tooling::Replacement(SM, DelRange, ""))) Errors = llvm::joinErrors(std::move(Errors), std::move(Err)); }; DelAttr(FD->getAttr()); DelAttr(FD->getAttr()); auto DelKeyword = [&](tok::TokenKind Kind, SourceRange FromRange) { bool FoundAny = false; for (const auto &Tok : TokBuf.expandedTokens(FromRange)) { if (Tok.kind() != Kind) continue; FoundAny = true; auto Spelling = TokBuf.spelledForExpanded(llvm::makeArrayRef(Tok)); if (!Spelling) { Errors = llvm::joinErrors( std::move(Errors), error("define outline: couldn't remove `{0}` keyword.", tok::getKeywordSpelling(Kind))); break; } CharSourceRange DelRange = syntax::Token::range(SM, Spelling->front(), Spelling->back()) .toCharRange(SM); if (auto Err = DeclarationCleanups.add(tooling::Replacement(SM, DelRange, ""))) Errors = llvm::joinErrors(std::move(Errors), std::move(Err)); } if (!FoundAny) { Errors = llvm::joinErrors( std::move(Errors), error("define outline: couldn't find `{0}` keyword to remove.", tok::getKeywordSpelling(Kind))); } }; if (const auto *MD = dyn_cast(FD)) { if (MD->isVirtualAsWritten()) DelKeyword(tok::kw_virtual, {FD->getBeginLoc(), FD->getLocation()}); if (MD->isStatic()) DelKeyword(tok::kw_static, {FD->getBeginLoc(), FD->getLocation()}); } if (Errors) return std::move(Errors); return getFunctionSourceAfterReplacements(FD, DeclarationCleanups); } struct InsertionPoint { std::string EnclosingNamespace; size_t Offset; }; // Returns the most natural insertion point for \p QualifiedName in \p Contents. // This currently cares about only the namespace proximity, but in feature it // should also try to follow ordering of declarations. For example, if decls // come in order `foo, bar, baz` then this function should return some point // between foo and baz for inserting bar. llvm::Expected getInsertionPoint(llvm::StringRef Contents, llvm::StringRef QualifiedName, const LangOptions &LangOpts) { auto Region = getEligiblePoints(Contents, QualifiedName, LangOpts); assert(!Region.EligiblePoints.empty()); // FIXME: This selection can be made smarter by looking at the definition // locations for adjacent decls to Source. Unfortunately pseudo parsing in // getEligibleRegions only knows about namespace begin/end events so we // can't match function start/end positions yet. auto Offset = positionToOffset(Contents, Region.EligiblePoints.back()); if (!Offset) return Offset.takeError(); return InsertionPoint{Region.EnclosingNamespace, *Offset}; } // Returns the range that should be deleted from declaration, which always // contains function body. In addition to that it might contain constructor // initializers. SourceRange getDeletionRange(const FunctionDecl *FD, const syntax::TokenBuffer &TokBuf) { auto DeletionRange = FD->getBody()->getSourceRange(); if (auto *CD = llvm::dyn_cast(FD)) { // AST doesn't contain the location for ":" in ctor initializers. Therefore // we find it by finding the first ":" before the first ctor initializer. SourceLocation InitStart; // Find the first initializer. for (const auto *CInit : CD->inits()) { // SourceOrder is -1 for implicit initializers. if (CInit->getSourceOrder() != 0) continue; InitStart = CInit->getSourceLocation(); break; } if (InitStart.isValid()) { auto Toks = TokBuf.expandedTokens(CD->getSourceRange()); // Drop any tokens after the initializer. Toks = Toks.take_while([&TokBuf, &InitStart](const syntax::Token &Tok) { return TokBuf.sourceManager().isBeforeInTranslationUnit(Tok.location(), InitStart); }); // Look for the first colon. auto Tok = llvm::find_if(llvm::reverse(Toks), [](const syntax::Token &Tok) { return Tok.kind() == tok::colon; }); assert(Tok != Toks.rend()); DeletionRange.setBegin(Tok->location()); } } return DeletionRange; } /// Moves definition of a function/method to an appropriate implementation file. /// /// Before: /// a.h /// void foo() { return; } /// a.cc /// #include "a.h" /// /// ---------------- /// /// After: /// a.h /// void foo(); /// a.cc /// #include "a.h" /// void foo() { return; } class DefineOutline : public Tweak { public: const char *id() const override; bool hidden() const override { return false; } - Intent intent() const override { return Intent::Refactor; } + llvm::StringLiteral kind() const override { + return CodeAction::REFACTOR_KIND; + } std::string title() const override { return "Move function body to out-of-line."; } bool prepare(const Selection &Sel) override { // Bail out if we are not in a header file. // FIXME: We might want to consider moving method definitions below class // definition even if we are inside a source file. if (!isHeaderFile(Sel.AST->getSourceManager().getFilename(Sel.Cursor), Sel.AST->getLangOpts())) return false; Source = getSelectedFunction(Sel.ASTSelection.commonAncestor()); // Bail out if the selection is not a in-line function definition. if (!Source || !Source->doesThisDeclarationHaveABody() || Source->isOutOfLine()) return false; // Bail out if this is a function template or specialization, as their // definitions need to be visible in all including translation units. if (Source->getDescribedFunctionTemplate()) return false; if (Source->getTemplateSpecializationInfo()) return false; // Bail out in templated classes, as it is hard to spell the class name, i.e // if the template parameter is unnamed. if (auto *MD = llvm::dyn_cast(Source)) { if (MD->getParent()->isTemplated()) return false; } // Note that we don't check whether an implementation file exists or not in // the prepare, since performing disk IO on each prepare request might be // expensive. return true; } Expected apply(const Selection &Sel) override { const SourceManager &SM = Sel.AST->getSourceManager(); auto MainFileName = getCanonicalPath(SM.getFileEntryForID(SM.getMainFileID()), SM); if (!MainFileName) return error("Couldn't get absolute path for main file."); auto CCFile = getSourceFile(*MainFileName, Sel); if (!CCFile) return error("Couldn't find a suitable implementation file."); auto &FS = Sel.AST->getSourceManager().getFileManager().getVirtualFileSystem(); auto Buffer = FS.getBufferForFile(*CCFile); // FIXME: Maybe we should consider creating the implementation file if it // doesn't exist? if (!Buffer) return llvm::errorCodeToError(Buffer.getError()); auto Contents = Buffer->get()->getBuffer(); auto InsertionPoint = getInsertionPoint( Contents, Source->getQualifiedNameAsString(), Sel.AST->getLangOpts()); if (!InsertionPoint) return InsertionPoint.takeError(); auto FuncDef = getFunctionSourceCode( Source, InsertionPoint->EnclosingNamespace, Sel.AST->getTokens()); if (!FuncDef) return FuncDef.takeError(); SourceManagerForFile SMFF(*CCFile, Contents); const tooling::Replacement InsertFunctionDef( *CCFile, InsertionPoint->Offset, 0, *FuncDef); auto Effect = Effect::mainFileEdit( SMFF.get(), tooling::Replacements(InsertFunctionDef)); if (!Effect) return Effect.takeError(); // FIXME: We should also get rid of inline qualifier. const tooling::Replacement DeleteFuncBody( Sel.AST->getSourceManager(), CharSourceRange::getTokenRange(*toHalfOpenFileRange( SM, Sel.AST->getLangOpts(), getDeletionRange(Source, Sel.AST->getTokens()))), ";"); auto HeaderFE = Effect::fileEdit(SM, SM.getMainFileID(), tooling::Replacements(DeleteFuncBody)); if (!HeaderFE) return HeaderFE.takeError(); Effect->ApplyEdits.try_emplace(HeaderFE->first, std::move(HeaderFE->second)); return std::move(*Effect); } private: const FunctionDecl *Source = nullptr; }; REGISTER_TWEAK(DefineOutline) } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/refactor/tweaks/DumpAST.cpp b/clang-tools-extra/clangd/refactor/tweaks/DumpAST.cpp index b2b883d64567..72e7cd5a2527 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/DumpAST.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/DumpAST.cpp @@ -1,170 +1,170 @@ //===--- DumpAST.cpp ---------------------------------------------*- C++-*-===// // // 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 // //===----------------------------------------------------------------------===// // Defines a few tweaks that expose AST and related information. // Some of these are fairly clang-specific and hidden (e.g. textual AST dumps). // Others are more generally useful (class layout) and are exposed by default. //===----------------------------------------------------------------------===// #include "XRefs.h" #include "refactor/Tweak.h" #include "clang/AST/ASTTypeTraits.h" #include "clang/AST/Type.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/ScopedPrinter.h" #include "llvm/Support/raw_ostream.h" namespace clang { namespace clangd { namespace { /// Dumps the AST of the selected node. /// Input: /// fcall("foo"); /// ^^^^^ /// Message: /// CallExpr /// |-DeclRefExpr fcall /// `-StringLiteral "foo" class DumpAST : public Tweak { public: const char *id() const override final; bool prepare(const Selection &Inputs) override { for (auto N = Inputs.ASTSelection.commonAncestor(); N && !Node; N = N->Parent) if (dumpable(N->ASTNode)) Node = N->ASTNode; return Node.hasValue(); } Expected apply(const Selection &Inputs) override; std::string title() const override { return std::string( llvm::formatv("Dump {0} AST", Node->getNodeKind().asStringRef())); } - Intent intent() const override { return Info; } + llvm::StringLiteral kind() const override { return CodeAction::INFO_KIND; } bool hidden() const override { return true; } private: static bool dumpable(const ast_type_traits::DynTypedNode &N) { // Sadly not all node types can be dumped, and there's no API to check. // See DynTypedNode::dump(). return N.get() || N.get() || N.get(); } llvm::Optional Node; }; REGISTER_TWEAK(DumpAST) llvm::Expected DumpAST::apply(const Selection &Inputs) { std::string Str; llvm::raw_string_ostream OS(Str); Node->dump(OS, Inputs.AST->getASTContext()); return Effect::showMessage(std::move(OS.str())); } /// Dumps the SelectionTree. /// Input: /// int fcall(int); /// void foo() { /// fcall(2 + 2); /// ^^^^^ /// } /// Message: /// TranslationUnitDecl /// FunctionDecl void foo() /// CompoundStmt {} /// .CallExpr fcall(2 + 2) /// ImplicitCastExpr fcall /// .DeclRefExpr fcall /// BinaryOperator 2 + 2 /// *IntegerLiteral 2 class ShowSelectionTree : public Tweak { public: const char *id() const override final; bool prepare(const Selection &Inputs) override { return true; } Expected apply(const Selection &Inputs) override { return Effect::showMessage(llvm::to_string(Inputs.ASTSelection)); } std::string title() const override { return "Show selection tree"; } - Intent intent() const override { return Info; } + llvm::StringLiteral kind() const override { return CodeAction::INFO_KIND; } bool hidden() const override { return true; } }; REGISTER_TWEAK(ShowSelectionTree) /// Dumps the symbol under the cursor. /// Inputs: /// void foo(); /// ^^^ /// Message: /// foo - /// {"containerName":null,"id":"CA2EBE44A1D76D2A","name":"foo","usr":"c:@F@foo#"} class DumpSymbol : public Tweak { const char *id() const override final; bool prepare(const Selection &Inputs) override { return true; } Expected apply(const Selection &Inputs) override { std::string Storage; llvm::raw_string_ostream Out(Storage); for (auto &Sym : getSymbolInfo( *Inputs.AST, sourceLocToPosition(Inputs.AST->getSourceManager(), Inputs.Cursor))) Out << Sym; return Effect::showMessage(Out.str()); } std::string title() const override { return "Dump symbol under the cursor"; } - Intent intent() const override { return Info; } + llvm::StringLiteral kind() const override { return CodeAction::INFO_KIND; } bool hidden() const override { return true; } }; REGISTER_TWEAK(DumpSymbol) /// Shows the layout of the RecordDecl under the cursor. /// Input: /// struct X { int foo; }; /// ^^^^^^^^ /// Message: /// 0 | struct S /// 0 | int foo /// | [sizeof=4, dsize=4, align=4, /// | nvsize=4, nvalign=4] class DumpRecordLayout : public Tweak { public: const char *id() const override final; bool prepare(const Selection &Inputs) override { if (auto *Node = Inputs.ASTSelection.commonAncestor()) if (auto *D = Node->ASTNode.get()) Record = dyn_cast(D); return Record && Record->isThisDeclarationADefinition() && !Record->isDependentType(); } Expected apply(const Selection &Inputs) override { std::string Str; llvm::raw_string_ostream OS(Str); Inputs.AST->getASTContext().DumpRecordLayout(Record, OS); return Effect::showMessage(std::move(OS.str())); } std::string title() const override { return std::string(llvm::formatv( "Show {0} layout", TypeWithKeyword::getTagTypeKindName(Record->getTagKind()))); } - Intent intent() const override { return Info; } + llvm::StringLiteral kind() const override { return CodeAction::INFO_KIND; } // FIXME: this is interesting to most users. However: // - triggering is too broad (e.g. triggers on comments within a class) // - showMessage has inconsistent UX (e.g. newlines are stripped in VSCode) // - the output itself is a bit hard to decipher. bool hidden() const override { return true; } private: const RecordDecl *Record = nullptr; }; REGISTER_TWEAK(DumpRecordLayout) } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/refactor/tweaks/ExpandAutoType.cpp b/clang-tools-extra/clangd/refactor/tweaks/ExpandAutoType.cpp index f9db50d934b0..4dfaf729c892 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/ExpandAutoType.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/ExpandAutoType.cpp @@ -1,103 +1,105 @@ //===--- ExpandAutoType.cpp --------------------------------------*- C++-*-===// // // 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 "refactor/Tweak.h" #include "XRefs.h" #include "support/Logger.h" #include "clang/AST/Type.h" #include "clang/AST/TypeLoc.h" #include "clang/Basic/LLVM.h" #include "llvm/ADT/None.h" #include "llvm/ADT/Optional.h" #include "llvm/ADT/StringExtras.h" #include "llvm/Support/Debug.h" #include "llvm/Support/Error.h" #include #include #include #include namespace clang { namespace clangd { namespace { /// Expand the "auto" type to the derived type /// Before: /// auto x = Something(); /// ^^^^ /// After: /// MyClass x = Something(); /// ^^^^^^^ /// FIXME: Handle decltype as well class ExpandAutoType : public Tweak { public: const char *id() const final; - Intent intent() const override { return Intent::Refactor;} + llvm::StringLiteral kind() const override { + return CodeAction::REFACTOR_KIND; + } bool prepare(const Selection &Inputs) override; Expected apply(const Selection &Inputs) override; std::string title() const override; private: /// Cache the AutoTypeLoc, so that we do not need to search twice. llvm::Optional CachedLocation; }; REGISTER_TWEAK(ExpandAutoType) std::string ExpandAutoType::title() const { return "Expand auto type"; } bool ExpandAutoType::prepare(const Selection& Inputs) { CachedLocation = llvm::None; if (auto *Node = Inputs.ASTSelection.commonAncestor()) { if (auto *TypeNode = Node->ASTNode.get()) { if (const AutoTypeLoc Result = TypeNode->getAs()) { // Code in apply() does handle 'decltype(auto)' yet. if (!Result.getTypePtr()->isDecltypeAuto()) CachedLocation = Result; } } } return (bool) CachedLocation; } Expected ExpandAutoType::apply(const Selection& Inputs) { auto &SrcMgr = Inputs.AST->getSourceManager(); llvm::Optional DeducedType = getDeducedType( Inputs.AST->getASTContext(), CachedLocation->getBeginLoc()); // if we can't resolve the type, return an error message if (DeducedType == llvm::None) return error("Could not deduce type for 'auto' type"); // if it's a lambda expression, return an error message if (isa(*DeducedType) && dyn_cast(*DeducedType)->getDecl()->isLambda()) { return error("Could not expand type of lambda expression"); } // if it's a function expression, return an error message // naively replacing 'auto' with the type will break declarations. // FIXME: there are other types that have similar problems if (DeducedType->getTypePtr()->isFunctionPointerType()) { return error("Could not expand type of function pointer"); } std::string PrettyTypeName = printType(*DeducedType, Inputs.ASTSelection.commonAncestor()->getDeclContext()); tooling::Replacement Expansion(SrcMgr, CharSourceRange(CachedLocation->getSourceRange(), true), PrettyTypeName); return Effect::mainFileEdit(SrcMgr, tooling::Replacements(Expansion)); } } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/refactor/tweaks/ExpandMacro.cpp b/clang-tools-extra/clangd/refactor/tweaks/ExpandMacro.cpp index 59a53f97c49c..a7e2dddf4cba 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/ExpandMacro.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/ExpandMacro.cpp @@ -1,134 +1,136 @@ //===--- ExpandMacro.cpp -----------------------------------------*- C++-*-===// // // 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 "refactor/Tweak.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" #include "clang/Basic/TokenKinds.h" #include "clang/Tooling/Core/Replacement.h" #include "clang/Tooling/Syntax/Tokens.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/STLExtras.h" #include "llvm/Support/Error.h" #include namespace clang { namespace clangd { namespace { /// Replaces a reference to a macro under the cursor with its expansion. /// Before: /// #define FOO(X) X+X /// FOO(10*a) /// ^^^ /// After: /// #define FOO(X) X+X /// 10*a+10*a class ExpandMacro : public Tweak { public: const char *id() const override final; - Intent intent() const override { return Intent::Refactor; } + llvm::StringLiteral kind() const override { + return CodeAction::REFACTOR_KIND; + } bool prepare(const Selection &Inputs) override; Expected apply(const Selection &Inputs) override; std::string title() const override; private: syntax::TokenBuffer::Expansion Expansion; std::string MacroName; }; REGISTER_TWEAK(ExpandMacro) /// Finds a spelled token that the cursor is pointing at. static const syntax::Token * findTokenUnderCursor(const SourceManager &SM, llvm::ArrayRef Spelled, unsigned CursorOffset) { // Find the token that strats after the offset, then look at a previous one. auto It = llvm::partition_point(Spelled, [&](const syntax::Token &T) { assert(T.location().isFileID()); return SM.getFileOffset(T.location()) <= CursorOffset; }); if (It == Spelled.begin()) return nullptr; // Check the token we found actually touches the cursor position. --It; return It->range(SM).touches(CursorOffset) ? It : nullptr; } static const syntax::Token * findIdentifierUnderCursor(const syntax::TokenBuffer &Tokens, SourceLocation Cursor) { assert(Cursor.isFileID()); auto &SM = Tokens.sourceManager(); auto Spelled = Tokens.spelledTokens(SM.getFileID(Cursor)); auto *T = findTokenUnderCursor(SM, Spelled, SM.getFileOffset(Cursor)); if (!T) return nullptr; if (T->kind() == tok::identifier) return T; // Also try the previous token when the cursor is at the boundary, e.g. // FOO^() // FOO^+ if (T == Spelled.begin()) return nullptr; --T; if (T->endLocation() != Cursor || T->kind() != tok::identifier) return nullptr; return T; } bool ExpandMacro::prepare(const Selection &Inputs) { // FIXME: we currently succeed on selection at the end of the token, e.g. // 'FOO[[ ]]BAR'. We should not trigger in that case. // Find a token under the cursor. auto *T = findIdentifierUnderCursor(Inputs.AST->getTokens(), Inputs.Cursor); // We are interested only in identifiers, other tokens can't be macro names. if (!T) return false; // If the identifier is a macro we will find the corresponding expansion. auto Expansion = Inputs.AST->getTokens().expansionStartingAt(T); if (!Expansion) return false; this->MacroName = std::string(T->text(Inputs.AST->getSourceManager())); this->Expansion = *Expansion; return true; } Expected ExpandMacro::apply(const Selection &Inputs) { auto &SM = Inputs.AST->getSourceManager(); std::string Replacement; for (const syntax::Token &T : Expansion.Expanded) { Replacement += T.text(SM); Replacement += " "; } if (!Replacement.empty()) { assert(Replacement.back() == ' '); Replacement.pop_back(); } CharSourceRange MacroRange = CharSourceRange::getCharRange(Expansion.Spelled.front().location(), Expansion.Spelled.back().endLocation()); tooling::Replacements Reps; llvm::cantFail(Reps.add(tooling::Replacement(SM, MacroRange, Replacement))); return Effect::mainFileEdit(SM, std::move(Reps)); } std::string ExpandMacro::title() const { return std::string(llvm::formatv("Expand macro '{0}'", MacroName)); } } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/refactor/tweaks/ExtractFunction.cpp b/clang-tools-extra/clangd/refactor/tweaks/ExtractFunction.cpp index 6ee5aee37f51..1ba8c3c1d9ff 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/ExtractFunction.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/ExtractFunction.cpp @@ -1,700 +1,702 @@ //===--- ExtractFunction.cpp -------------------------------------*- C++-*-===// // // 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 // //===----------------------------------------------------------------------===// // // Extracts statements to a new function and replaces the statements with a // call to the new function. // Before: // void f(int a) { // [[if(a < 5) // a = 5;]] // } // After: // void extracted(int &a) { // if(a < 5) // a = 5; // } // void f(int a) { // extracted(a); // } // // - Only extract statements // - Extracts from non-templated free functions only. // - Parameters are const only if the declaration was const // - Always passed by l-value reference // - Void return type // - Cannot extract declarations that will be needed in the original function // after extraction. // - Checks for broken control flow (break/continue without loop/switch) // // 1. ExtractFunction is the tweak subclass // - Prepare does basic analysis of the selection and is therefore fast. // Successful prepare doesn't always mean we can apply the tweak. // - Apply does a more detailed analysis and can be slower. In case of // failure, we let the user know that we are unable to perform extraction. // 2. ExtractionZone store information about the range being extracted and the // enclosing function. // 3. NewFunction stores properties of the extracted function and provides // methods for rendering it. // 4. CapturedZoneInfo uses a RecursiveASTVisitor to capture information about // the extraction like declarations, existing return statements, etc. // 5. getExtractedFunction is responsible for analyzing the CapturedZoneInfo and // creating a NewFunction. //===----------------------------------------------------------------------===// #include "AST.h" #include "ParsedAST.h" #include "Selection.h" #include "SourceCode.h" #include "refactor/Tweak.h" #include "support/Logger.h" #include "clang/AST/ASTContext.h" #include "clang/AST/Decl.h" #include "clang/AST/DeclTemplate.h" #include "clang/AST/RecursiveASTVisitor.h" #include "clang/AST/Stmt.h" #include "clang/Basic/LangOptions.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" #include "clang/Lex/Lexer.h" #include "clang/Tooling/Core/Replacement.h" #include "clang/Tooling/Refactoring/Extract/SourceExtraction.h" #include "llvm/ADT/None.h" #include "llvm/ADT/Optional.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/iterator_range.h" #include "llvm/Support/Casting.h" #include "llvm/Support/Error.h" namespace clang { namespace clangd { namespace { using Node = SelectionTree::Node; // ExtractionZone is the part of code that is being extracted. // EnclosingFunction is the function/method inside which the zone lies. // We split the file into 4 parts relative to extraction zone. enum class ZoneRelative { Before, // Before Zone and inside EnclosingFunction. Inside, // Inside Zone. After, // After Zone and inside EnclosingFunction. OutsideFunc // Outside EnclosingFunction. }; // A RootStmt is a statement that's fully selected including all it's children // and it's parent is unselected. // Check if a node is a root statement. bool isRootStmt(const Node *N) { if (!N->ASTNode.get()) return false; // Root statement cannot be partially selected. if (N->Selected == SelectionTree::Partial) return false; // Only DeclStmt can be an unselected RootStmt since VarDecls claim the entire // selection range in selectionTree. if (N->Selected == SelectionTree::Unselected && !N->ASTNode.get()) return false; return true; } // Returns the (unselected) parent of all RootStmts given the commonAncestor. // Returns null if: // 1. any node is partially selected // 2. If all completely selected nodes don't have the same common parent // 3. Any child of Parent isn't a RootStmt. // Returns null if any child is not a RootStmt. // We only support extraction of RootStmts since it allows us to extract without // having to change the selection range. Also, this means that any scope that // begins in selection range, ends in selection range and any scope that begins // outside the selection range, ends outside as well. const Node *getParentOfRootStmts(const Node *CommonAnc) { if (!CommonAnc) return nullptr; const Node *Parent = nullptr; switch (CommonAnc->Selected) { case SelectionTree::Selection::Unselected: // Typically a block, with the { and } unselected, could also be ForStmt etc // Ensure all Children are RootStmts. Parent = CommonAnc; break; case SelectionTree::Selection::Partial: // Only a fully-selected single statement can be selected. return nullptr; case SelectionTree::Selection::Complete: // If the Common Ancestor is completely selected, then it's a root statement // and its parent will be unselected. Parent = CommonAnc->Parent; // If parent is a DeclStmt, even though it's unselected, we consider it a // root statement and return its parent. This is done because the VarDecls // claim the entire selection range of the Declaration and DeclStmt is // always unselected. if (Parent->ASTNode.get()) Parent = Parent->Parent; break; } // Ensure all Children are RootStmts. return llvm::all_of(Parent->Children, isRootStmt) ? Parent : nullptr; } // The ExtractionZone class forms a view of the code wrt Zone. struct ExtractionZone { // Parent of RootStatements being extracted. const Node *Parent = nullptr; // The half-open file range of the code being extracted. SourceRange ZoneRange; // The function inside which our zone resides. const FunctionDecl *EnclosingFunction = nullptr; // The half-open file range of the enclosing function. SourceRange EnclosingFuncRange; SourceLocation getInsertionPoint() const { return EnclosingFuncRange.getBegin(); } bool isRootStmt(const Stmt *S) const; // The last root statement is important to decide where we need to insert a // semicolon after the extraction. const Node *getLastRootStmt() const { return Parent->Children.back(); } void generateRootStmts(); private: llvm::DenseSet RootStmts; }; // Whether the code in the extraction zone is guaranteed to return, assuming // no broken control flow (unbound break/continue). // This is a very naive check (does it end with a return stmt). // Doing some rudimentary control flow analysis would cover more cases. bool alwaysReturns(const ExtractionZone &EZ) { const Stmt *Last = EZ.getLastRootStmt()->ASTNode.get(); // Unwrap enclosing (unconditional) compound statement. while (const auto *CS = llvm::dyn_cast(Last)) { if (CS->body_empty()) return false; else Last = CS->body_back(); } return llvm::isa(Last); } bool ExtractionZone::isRootStmt(const Stmt *S) const { return RootStmts.find(S) != RootStmts.end(); } // Generate RootStmts set void ExtractionZone::generateRootStmts() { for (const Node *Child : Parent->Children) RootStmts.insert(Child->ASTNode.get()); } // Finds the function in which the zone lies. const FunctionDecl *findEnclosingFunction(const Node *CommonAnc) { // Walk up the SelectionTree until we find a function Decl for (const Node *CurNode = CommonAnc; CurNode; CurNode = CurNode->Parent) { // Don't extract from lambdas if (CurNode->ASTNode.get()) return nullptr; if (const FunctionDecl *Func = CurNode->ASTNode.get()) { // FIXME: Support extraction from methods. if (isa(Func)) return nullptr; // FIXME: Support extraction from templated functions. if (Func->isTemplated()) return nullptr; return Func; } } return nullptr; } // Zone Range is the union of SourceRanges of all child Nodes in Parent since // all child Nodes are RootStmts llvm::Optional findZoneRange(const Node *Parent, const SourceManager &SM, const LangOptions &LangOpts) { SourceRange SR; if (auto BeginFileRange = toHalfOpenFileRange( SM, LangOpts, Parent->Children.front()->ASTNode.getSourceRange())) SR.setBegin(BeginFileRange->getBegin()); else return llvm::None; if (auto EndFileRange = toHalfOpenFileRange( SM, LangOpts, Parent->Children.back()->ASTNode.getSourceRange())) SR.setEnd(EndFileRange->getEnd()); else return llvm::None; return SR; } // Compute the range spanned by the enclosing function. // FIXME: check if EnclosingFunction has any attributes as the AST doesn't // always store the source range of the attributes and thus we end up extracting // between the attributes and the EnclosingFunction. llvm::Optional computeEnclosingFuncRange(const FunctionDecl *EnclosingFunction, const SourceManager &SM, const LangOptions &LangOpts) { return toHalfOpenFileRange(SM, LangOpts, EnclosingFunction->getSourceRange()); } // returns true if Child can be a single RootStmt being extracted from // EnclosingFunc. bool validSingleChild(const Node *Child, const FunctionDecl *EnclosingFunc) { // Don't extract expressions. // FIXME: We should extract expressions that are "statements" i.e. not // subexpressions if (Child->ASTNode.get()) return false; // Extracting the body of EnclosingFunc would remove it's definition. assert(EnclosingFunc->hasBody() && "We should always be extracting from a function body."); if (Child->ASTNode.get() == EnclosingFunc->getBody()) return false; return true; } // FIXME: Check we're not extracting from the initializer/condition of a control // flow structure. llvm::Optional findExtractionZone(const Node *CommonAnc, const SourceManager &SM, const LangOptions &LangOpts) { ExtractionZone ExtZone; ExtZone.Parent = getParentOfRootStmts(CommonAnc); if (!ExtZone.Parent || ExtZone.Parent->Children.empty()) return llvm::None; ExtZone.EnclosingFunction = findEnclosingFunction(ExtZone.Parent); if (!ExtZone.EnclosingFunction) return llvm::None; // When there is a single RootStmt, we must check if it's valid for // extraction. if (ExtZone.Parent->Children.size() == 1 && !validSingleChild(ExtZone.getLastRootStmt(), ExtZone.EnclosingFunction)) return llvm::None; if (auto FuncRange = computeEnclosingFuncRange(ExtZone.EnclosingFunction, SM, LangOpts)) ExtZone.EnclosingFuncRange = *FuncRange; if (auto ZoneRange = findZoneRange(ExtZone.Parent, SM, LangOpts)) ExtZone.ZoneRange = *ZoneRange; if (ExtZone.EnclosingFuncRange.isInvalid() || ExtZone.ZoneRange.isInvalid()) return llvm::None; ExtZone.generateRootStmts(); return ExtZone; } // Stores information about the extracted function and provides methods for // rendering it. struct NewFunction { struct Parameter { std::string Name; QualType TypeInfo; bool PassByReference; unsigned OrderPriority; // Lower value parameters are preferred first. std::string render(const DeclContext *Context) const; bool operator<(const Parameter &Other) const { return OrderPriority < Other.OrderPriority; } }; std::string Name = "extracted"; QualType ReturnType; std::vector Parameters; SourceRange BodyRange; SourceLocation InsertionPoint; const DeclContext *EnclosingFuncContext; bool CallerReturnsValue = false; // Decides whether the extracted function body and the function call need a // semicolon after extraction. tooling::ExtractionSemicolonPolicy SemicolonPolicy; NewFunction(tooling::ExtractionSemicolonPolicy SemicolonPolicy) : SemicolonPolicy(SemicolonPolicy) {} // Render the call for this function. std::string renderCall() const; // Render the definition for this function. std::string renderDefinition(const SourceManager &SM) const; private: std::string renderParametersForDefinition() const; std::string renderParametersForCall() const; // Generate the function body. std::string getFuncBody(const SourceManager &SM) const; }; std::string NewFunction::renderParametersForDefinition() const { std::string Result; bool NeedCommaBefore = false; for (const Parameter &P : Parameters) { if (NeedCommaBefore) Result += ", "; NeedCommaBefore = true; Result += P.render(EnclosingFuncContext); } return Result; } std::string NewFunction::renderParametersForCall() const { std::string Result; bool NeedCommaBefore = false; for (const Parameter &P : Parameters) { if (NeedCommaBefore) Result += ", "; NeedCommaBefore = true; Result += P.Name; } return Result; } std::string NewFunction::renderCall() const { return std::string( llvm::formatv("{0}{1}({2}){3}", CallerReturnsValue ? "return " : "", Name, renderParametersForCall(), (SemicolonPolicy.isNeededInOriginalFunction() ? ";" : ""))); } std::string NewFunction::renderDefinition(const SourceManager &SM) const { return std::string(llvm::formatv( "{0} {1}({2}) {\n{3}\n}\n", printType(ReturnType, *EnclosingFuncContext), Name, renderParametersForDefinition(), getFuncBody(SM))); } std::string NewFunction::getFuncBody(const SourceManager &SM) const { // FIXME: Generate tooling::Replacements instead of std::string to // - hoist decls // - add return statement // - Add semicolon return toSourceCode(SM, BodyRange).str() + (SemicolonPolicy.isNeededInExtractedFunction() ? ";" : ""); } std::string NewFunction::Parameter::render(const DeclContext *Context) const { return printType(TypeInfo, *Context) + (PassByReference ? " &" : " ") + Name; } // Stores captured information about Extraction Zone. struct CapturedZoneInfo { struct DeclInformation { const Decl *TheDecl; ZoneRelative DeclaredIn; // index of the declaration or first reference. unsigned DeclIndex; bool IsReferencedInZone = false; bool IsReferencedInPostZone = false; // FIXME: Capture mutation information DeclInformation(const Decl *TheDecl, ZoneRelative DeclaredIn, unsigned DeclIndex) : TheDecl(TheDecl), DeclaredIn(DeclaredIn), DeclIndex(DeclIndex){}; // Marks the occurence of a reference for this declaration void markOccurence(ZoneRelative ReferenceLoc); }; // Maps Decls to their DeclInfo llvm::DenseMap DeclInfoMap; bool HasReturnStmt = false; // Are there any return statements in the zone? bool AlwaysReturns = false; // Does the zone always return? // Control flow is broken if we are extracting a break/continue without a // corresponding parent loop/switch bool BrokenControlFlow = false; // FIXME: capture TypeAliasDecl and UsingDirectiveDecl // FIXME: Capture type information as well. DeclInformation *createDeclInfo(const Decl *D, ZoneRelative RelativeLoc); DeclInformation *getDeclInfoFor(const Decl *D); }; CapturedZoneInfo::DeclInformation * CapturedZoneInfo::createDeclInfo(const Decl *D, ZoneRelative RelativeLoc) { // The new Decl's index is the size of the map so far. auto InsertionResult = DeclInfoMap.insert( {D, DeclInformation(D, RelativeLoc, DeclInfoMap.size())}); // Return the newly created DeclInfo return &InsertionResult.first->second; } CapturedZoneInfo::DeclInformation * CapturedZoneInfo::getDeclInfoFor(const Decl *D) { // If the Decl doesn't exist, we auto Iter = DeclInfoMap.find(D); if (Iter == DeclInfoMap.end()) return nullptr; return &Iter->second; } void CapturedZoneInfo::DeclInformation::markOccurence( ZoneRelative ReferenceLoc) { switch (ReferenceLoc) { case ZoneRelative::Inside: IsReferencedInZone = true; break; case ZoneRelative::After: IsReferencedInPostZone = true; break; default: break; } } bool isLoop(const Stmt *S) { return isa(S) || isa(S) || isa(S) || isa(S); } // Captures information from Extraction Zone CapturedZoneInfo captureZoneInfo(const ExtractionZone &ExtZone) { // We use the ASTVisitor instead of using the selection tree since we need to // find references in the PostZone as well. // FIXME: Check which statements we don't allow to extract. class ExtractionZoneVisitor : public clang::RecursiveASTVisitor { public: ExtractionZoneVisitor(const ExtractionZone &ExtZone) : ExtZone(ExtZone) { TraverseDecl(const_cast(ExtZone.EnclosingFunction)); } bool TraverseStmt(Stmt *S) { if (!S) return true; bool IsRootStmt = ExtZone.isRootStmt(const_cast(S)); // If we are starting traversal of a RootStmt, we are somewhere inside // ExtractionZone if (IsRootStmt) CurrentLocation = ZoneRelative::Inside; addToLoopSwitchCounters(S, 1); // Traverse using base class's TraverseStmt RecursiveASTVisitor::TraverseStmt(S); addToLoopSwitchCounters(S, -1); // We set the current location as after since next stmt will either be a // RootStmt (handled at the beginning) or after extractionZone if (IsRootStmt) CurrentLocation = ZoneRelative::After; return true; } // Add Increment to CurNumberOf{Loops,Switch} if statement is // {Loop,Switch} and inside Extraction Zone. void addToLoopSwitchCounters(Stmt *S, int Increment) { if (CurrentLocation != ZoneRelative::Inside) return; if (isLoop(S)) CurNumberOfNestedLoops += Increment; else if (isa(S)) CurNumberOfSwitch += Increment; } bool VisitDecl(Decl *D) { Info.createDeclInfo(D, CurrentLocation); return true; } bool VisitDeclRefExpr(DeclRefExpr *DRE) { // Find the corresponding Decl and mark it's occurrence. const Decl *D = DRE->getDecl(); auto *DeclInfo = Info.getDeclInfoFor(D); // If no Decl was found, the Decl must be outside the enclosingFunc. if (!DeclInfo) DeclInfo = Info.createDeclInfo(D, ZoneRelative::OutsideFunc); DeclInfo->markOccurence(CurrentLocation); // FIXME: check if reference mutates the Decl being referred. return true; } bool VisitReturnStmt(ReturnStmt *Return) { if (CurrentLocation == ZoneRelative::Inside) Info.HasReturnStmt = true; return true; } bool VisitBreakStmt(BreakStmt *Break) { // Control flow is broken if break statement is selected without any // parent loop or switch statement. if (CurrentLocation == ZoneRelative::Inside && !(CurNumberOfNestedLoops || CurNumberOfSwitch)) Info.BrokenControlFlow = true; return true; } bool VisitContinueStmt(ContinueStmt *Continue) { // Control flow is broken if Continue statement is selected without any // parent loop if (CurrentLocation == ZoneRelative::Inside && !CurNumberOfNestedLoops) Info.BrokenControlFlow = true; return true; } CapturedZoneInfo Info; const ExtractionZone &ExtZone; ZoneRelative CurrentLocation = ZoneRelative::Before; // Number of {loop,switch} statements that are currently in the traversal // stack inside Extraction Zone. Used to check for broken control flow. unsigned CurNumberOfNestedLoops = 0; unsigned CurNumberOfSwitch = 0; }; ExtractionZoneVisitor Visitor(ExtZone); CapturedZoneInfo Result = std::move(Visitor.Info); Result.AlwaysReturns = alwaysReturns(ExtZone); return Result; } // Adds parameters to ExtractedFunc. // Returns true if able to find the parameters successfully and no hoisting // needed. // FIXME: Check if the declaration has a local/anonymous type bool createParameters(NewFunction &ExtractedFunc, const CapturedZoneInfo &CapturedInfo) { for (const auto &KeyVal : CapturedInfo.DeclInfoMap) { const auto &DeclInfo = KeyVal.second; // If a Decl was Declared in zone and referenced in post zone, it // needs to be hoisted (we bail out in that case). // FIXME: Support Decl Hoisting. if (DeclInfo.DeclaredIn == ZoneRelative::Inside && DeclInfo.IsReferencedInPostZone) return false; if (!DeclInfo.IsReferencedInZone) continue; // no need to pass as parameter, not referenced if (DeclInfo.DeclaredIn == ZoneRelative::Inside || DeclInfo.DeclaredIn == ZoneRelative::OutsideFunc) continue; // no need to pass as parameter, still accessible. // Parameter specific checks. const ValueDecl *VD = dyn_cast_or_null(DeclInfo.TheDecl); // Can't parameterise if the Decl isn't a ValueDecl or is a FunctionDecl // (this includes the case of recursive call to EnclosingFunc in Zone). if (!VD || isa(DeclInfo.TheDecl)) return false; // Parameter qualifiers are same as the Decl's qualifiers. QualType TypeInfo = VD->getType().getNonReferenceType(); // FIXME: Need better qualifier checks: check mutated status for // Decl(e.g. was it assigned, passed as nonconst argument, etc) // FIXME: check if parameter will be a non l-value reference. // FIXME: We don't want to always pass variables of types like int, // pointers, etc by reference. bool IsPassedByReference = true; // We use the index of declaration as the ordering priority for parameters. ExtractedFunc.Parameters.push_back({std::string(VD->getName()), TypeInfo, IsPassedByReference, DeclInfo.DeclIndex}); } llvm::sort(ExtractedFunc.Parameters); return true; } // Clangd uses open ranges while ExtractionSemicolonPolicy (in Clang Tooling) // uses closed ranges. Generates the semicolon policy for the extraction and // extends the ZoneRange if necessary. tooling::ExtractionSemicolonPolicy getSemicolonPolicy(ExtractionZone &ExtZone, const SourceManager &SM, const LangOptions &LangOpts) { // Get closed ZoneRange. SourceRange FuncBodyRange = {ExtZone.ZoneRange.getBegin(), ExtZone.ZoneRange.getEnd().getLocWithOffset(-1)}; auto SemicolonPolicy = tooling::ExtractionSemicolonPolicy::compute( ExtZone.getLastRootStmt()->ASTNode.get(), FuncBodyRange, SM, LangOpts); // Update ZoneRange. ExtZone.ZoneRange.setEnd(FuncBodyRange.getEnd().getLocWithOffset(1)); return SemicolonPolicy; } // Generate return type for ExtractedFunc. Return false if unable to do so. bool generateReturnProperties(NewFunction &ExtractedFunc, const FunctionDecl &EnclosingFunc, const CapturedZoneInfo &CapturedInfo) { // If the selected code always returns, we preserve those return statements. // The return type should be the same as the enclosing function. // (Others are possible if there are conversions, but this seems clearest). if (CapturedInfo.HasReturnStmt) { // If the return is conditional, neither replacing the code with // `extracted()` nor `return extracted()` is correct. if (!CapturedInfo.AlwaysReturns) return false; QualType Ret = EnclosingFunc.getReturnType(); // Once we support members, it'd be nice to support e.g. extracting a method // of Foo that returns T. But it's not clear when that's safe. if (Ret->isDependentType()) return false; ExtractedFunc.ReturnType = Ret; return true; } // FIXME: Generate new return statement if needed. ExtractedFunc.ReturnType = EnclosingFunc.getParentASTContext().VoidTy; return true; } // FIXME: add support for adding other function return types besides void. // FIXME: assign the value returned by non void extracted function. llvm::Expected getExtractedFunction(ExtractionZone &ExtZone, const SourceManager &SM, const LangOptions &LangOpts) { CapturedZoneInfo CapturedInfo = captureZoneInfo(ExtZone); // Bail out if any break of continue exists if (CapturedInfo.BrokenControlFlow) return error("Cannot extract break/continue without corresponding " "loop/switch statement."); NewFunction ExtractedFunc(getSemicolonPolicy(ExtZone, SM, LangOpts)); ExtractedFunc.BodyRange = ExtZone.ZoneRange; ExtractedFunc.InsertionPoint = ExtZone.getInsertionPoint(); ExtractedFunc.EnclosingFuncContext = ExtZone.EnclosingFunction->getDeclContext(); ExtractedFunc.CallerReturnsValue = CapturedInfo.AlwaysReturns; if (!createParameters(ExtractedFunc, CapturedInfo) || !generateReturnProperties(ExtractedFunc, *ExtZone.EnclosingFunction, CapturedInfo)) return error("Too complex to extract."); return ExtractedFunc; } class ExtractFunction : public Tweak { public: const char *id() const override final; bool prepare(const Selection &Inputs) override; Expected apply(const Selection &Inputs) override; std::string title() const override { return "Extract to function"; } - Intent intent() const override { return Refactor; } + llvm::StringLiteral kind() const override { + return CodeAction::REFACTOR_KIND; + } private: ExtractionZone ExtZone; }; REGISTER_TWEAK(ExtractFunction) tooling::Replacement replaceWithFuncCall(const NewFunction &ExtractedFunc, const SourceManager &SM, const LangOptions &LangOpts) { std::string FuncCall = ExtractedFunc.renderCall(); return tooling::Replacement( SM, CharSourceRange(ExtractedFunc.BodyRange, false), FuncCall, LangOpts); } tooling::Replacement createFunctionDefinition(const NewFunction &ExtractedFunc, const SourceManager &SM) { std::string FunctionDef = ExtractedFunc.renderDefinition(SM); return tooling::Replacement(SM, ExtractedFunc.InsertionPoint, 0, FunctionDef); } bool ExtractFunction::prepare(const Selection &Inputs) { const Node *CommonAnc = Inputs.ASTSelection.commonAncestor(); const SourceManager &SM = Inputs.AST->getSourceManager(); const LangOptions &LangOpts = Inputs.AST->getLangOpts(); if (!LangOpts.CPlusPlus) return false; if (auto MaybeExtZone = findExtractionZone(CommonAnc, SM, LangOpts)) { ExtZone = std::move(*MaybeExtZone); return true; } return false; } Expected ExtractFunction::apply(const Selection &Inputs) { const SourceManager &SM = Inputs.AST->getSourceManager(); const LangOptions &LangOpts = Inputs.AST->getLangOpts(); auto ExtractedFunc = getExtractedFunction(ExtZone, SM, LangOpts); // FIXME: Add more types of errors. if (!ExtractedFunc) return ExtractedFunc.takeError(); tooling::Replacements Result; if (auto Err = Result.add(createFunctionDefinition(*ExtractedFunc, SM))) return std::move(Err); if (auto Err = Result.add(replaceWithFuncCall(*ExtractedFunc, SM, LangOpts))) return std::move(Err); return Effect::mainFileEdit(SM, std::move(Result)); } } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/refactor/tweaks/ExtractVariable.cpp b/clang-tools-extra/clangd/refactor/tweaks/ExtractVariable.cpp index 104f8ba63dd0..8b668be5f2f9 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/ExtractVariable.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/ExtractVariable.cpp @@ -1,481 +1,483 @@ //===--- ExtractVariable.cpp ------------------------------------*- C++-*-===// // // 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 "ParsedAST.h" #include "Protocol.h" #include "Selection.h" #include "SourceCode.h" #include "refactor/Tweak.h" #include "support/Logger.h" #include "clang/AST/ASTContext.h" #include "clang/AST/Expr.h" #include "clang/AST/ExprCXX.h" #include "clang/AST/OperationKinds.h" #include "clang/AST/RecursiveASTVisitor.h" #include "clang/AST/Stmt.h" #include "clang/AST/StmtCXX.h" #include "clang/Basic/LangOptions.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/None.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Casting.h" #include "llvm/Support/Error.h" #include "llvm/Support/raw_ostream.h" namespace clang { namespace clangd { namespace { // information regarding the Expr that is being extracted class ExtractionContext { public: ExtractionContext(const SelectionTree::Node *Node, const SourceManager &SM, const ASTContext &Ctx); const clang::Expr *getExpr() const { return Expr; } const SelectionTree::Node *getExprNode() const { return ExprNode; } bool isExtractable() const { return Extractable; } // The half-open range for the expression to be extracted. SourceRange getExtractionChars() const; // Generate Replacement for replacing selected expression with given VarName tooling::Replacement replaceWithVar(SourceRange Chars, llvm::StringRef VarName) const; // Generate Replacement for declaring the selected Expr as a new variable tooling::Replacement insertDeclaration(llvm::StringRef VarName, SourceRange InitChars) const; private: bool Extractable = false; const clang::Expr *Expr; const SelectionTree::Node *ExprNode; // Stmt before which we will extract const clang::Stmt *InsertionPoint = nullptr; const SourceManager &SM; const ASTContext &Ctx; // Decls referenced in the Expr std::vector ReferencedDecls; // returns true if the Expr doesn't reference any variable declared in scope bool exprIsValidOutside(const clang::Stmt *Scope) const; // computes the Stmt before which we will extract out Expr const clang::Stmt *computeInsertionPoint() const; }; // Returns all the Decls referenced inside the given Expr static std::vector computeReferencedDecls(const clang::Expr *Expr) { // RAV subclass to find all DeclRefs in a given Stmt class FindDeclRefsVisitor : public clang::RecursiveASTVisitor { public: std::vector ReferencedDecls; bool VisitDeclRefExpr(DeclRefExpr *DeclRef) { // NOLINT ReferencedDecls.push_back(DeclRef->getDecl()); return true; } }; FindDeclRefsVisitor Visitor; Visitor.TraverseStmt(const_cast(dyn_cast(Expr))); return Visitor.ReferencedDecls; } ExtractionContext::ExtractionContext(const SelectionTree::Node *Node, const SourceManager &SM, const ASTContext &Ctx) : ExprNode(Node), SM(SM), Ctx(Ctx) { Expr = Node->ASTNode.get(); ReferencedDecls = computeReferencedDecls(Expr); InsertionPoint = computeInsertionPoint(); if (InsertionPoint) Extractable = true; } // checks whether extracting before InsertionPoint will take a // variable reference out of scope bool ExtractionContext::exprIsValidOutside(const clang::Stmt *Scope) const { SourceLocation ScopeBegin = Scope->getBeginLoc(); SourceLocation ScopeEnd = Scope->getEndLoc(); for (const Decl *ReferencedDecl : ReferencedDecls) { if (SM.isPointWithin(ReferencedDecl->getBeginLoc(), ScopeBegin, ScopeEnd) && SM.isPointWithin(ReferencedDecl->getEndLoc(), ScopeBegin, ScopeEnd)) return false; } return true; } // Return the Stmt before which we need to insert the extraction. // To find the Stmt, we go up the AST Tree and if the Parent of the current // Stmt is a CompoundStmt, we can extract inside this CompoundStmt just before // the current Stmt. We ALWAYS insert before a Stmt whose parent is a // CompoundStmt // // FIXME: Extraction from label, switch and case statements // FIXME: Doens't work for FoldExpr // FIXME: Ensure extraction from loops doesn't change semantics. const clang::Stmt *ExtractionContext::computeInsertionPoint() const { // returns true if we can extract before InsertionPoint auto CanExtractOutside = [](const SelectionTree::Node *InsertionPoint) -> bool { if (const clang::Stmt *Stmt = InsertionPoint->ASTNode.get()) { // Allow all expressions except LambdaExpr since we don't want to extract // from the captures/default arguments of a lambda if (isa(Stmt)) return !isa(Stmt); // We don't yet allow extraction from switch/case stmt as we would need to // jump over the switch stmt even if there is a CompoundStmt inside the // switch. And there are other Stmts which we don't care about (e.g. // continue and break) as there can never be anything to extract from // them. return isa(Stmt) || isa(Stmt) || isa(Stmt) || isa(Stmt) || isa(Stmt) || isa(Stmt) || isa(Stmt) || isa(Stmt) || isa(Stmt); } if (InsertionPoint->ASTNode.get()) return true; return false; }; for (const SelectionTree::Node *CurNode = getExprNode(); CurNode->Parent && CanExtractOutside(CurNode); CurNode = CurNode->Parent) { const clang::Stmt *CurInsertionPoint = CurNode->ASTNode.get(); // give up if extraction will take a variable out of scope if (CurInsertionPoint && !exprIsValidOutside(CurInsertionPoint)) break; if (const clang::Stmt *CurParent = CurNode->Parent->ASTNode.get()) { if (isa(CurParent)) { // Ensure we don't write inside a macro. if (CurParent->getBeginLoc().isMacroID()) continue; return CurInsertionPoint; } } } return nullptr; } // returns the replacement for substituting the extraction with VarName tooling::Replacement ExtractionContext::replaceWithVar(SourceRange Chars, llvm::StringRef VarName) const { unsigned ExtractionLength = SM.getFileOffset(Chars.getEnd()) - SM.getFileOffset(Chars.getBegin()); return tooling::Replacement(SM, Chars.getBegin(), ExtractionLength, VarName); } // returns the Replacement for declaring a new variable storing the extraction tooling::Replacement ExtractionContext::insertDeclaration(llvm::StringRef VarName, SourceRange InitializerChars) const { llvm::StringRef ExtractionCode = toSourceCode(SM, InitializerChars); const SourceLocation InsertionLoc = toHalfOpenFileRange(SM, Ctx.getLangOpts(), InsertionPoint->getSourceRange()) ->getBegin(); // FIXME: Replace auto with explicit type and add &/&& as necessary std::string ExtractedVarDecl = std::string("auto ") + VarName.str() + " = " + ExtractionCode.str() + "; "; return tooling::Replacement(SM, InsertionLoc, 0, ExtractedVarDecl); } // Helpers for handling "binary subexpressions" like a + [[b + c]] + d. // // These are special, because the formal AST doesn't match what users expect: // - the AST is ((a + b) + c) + d, so the ancestor expression is `a + b + c`. // - but extracting `b + c` is reasonable, as + is (mathematically) associative. // // So we try to support these cases with some restrictions: // - the operator must be associative // - no mixing of operators is allowed // - we don't look inside macro expansions in the subexpressions // - we only adjust the extracted range, so references in the unselected parts // of the AST expression (e.g. `a`) are still considered referenced for // the purposes of calculating the insertion point. // FIXME: it would be nice to exclude these references, by micromanaging // the computeReferencedDecls() calls around the binary operator tree. // Information extracted about a binary operator encounted in a SelectionTree. // It can represent either an overloaded or built-in operator. struct ParsedBinaryOperator { BinaryOperatorKind Kind; SourceLocation ExprLoc; llvm::SmallVector SelectedOperands; // If N is a binary operator, populate this and return true. bool parse(const SelectionTree::Node &N) { SelectedOperands.clear(); if (const BinaryOperator *Op = llvm::dyn_cast_or_null(N.ASTNode.get())) { Kind = Op->getOpcode(); ExprLoc = Op->getExprLoc(); SelectedOperands = N.Children; return true; } if (const CXXOperatorCallExpr *Op = llvm::dyn_cast_or_null( N.ASTNode.get())) { if (!Op->isInfixBinaryOp()) return false; Kind = BinaryOperator::getOverloadedOpcode(Op->getOperator()); ExprLoc = Op->getExprLoc(); // Not all children are args, there's also the callee (operator). for (const auto* Child : N.Children) { const Expr *E = Child->ASTNode.get(); assert(E && "callee and args should be Exprs!"); if (E == Op->getArg(0) || E == Op->getArg(1)) SelectedOperands.push_back(Child); } return true; } return false; } bool associative() const { // Must also be left-associative, or update getBinaryOperatorRange()! switch (Kind) { case BO_Add: case BO_Mul: case BO_And: case BO_Or: case BO_Xor: case BO_LAnd: case BO_LOr: return true; default: return false; } } bool crossesMacroBoundary(const SourceManager &SM) { FileID F = SM.getFileID(ExprLoc); for (const SelectionTree::Node *Child : SelectedOperands) if (SM.getFileID(Child->ASTNode.get()->getExprLoc()) != F) return true; return false; } }; // If have an associative operator at the top level, then we must find // the start point (rightmost in LHS) and end point (leftmost in RHS). // We can only descend into subtrees where the operator matches. // // e.g. for a + [[b + c]] + d // + // / \ // N-> + d // / \ // + c <- End // / \ // a b <- Start const SourceRange getBinaryOperatorRange(const SelectionTree::Node &N, const SourceManager &SM, const LangOptions &LangOpts) { // If N is not a suitable binary operator, bail out. ParsedBinaryOperator Op; if (!Op.parse(N.ignoreImplicit()) || !Op.associative() || Op.crossesMacroBoundary(SM) || Op.SelectedOperands.size() != 2) return SourceRange(); BinaryOperatorKind OuterOp = Op.Kind; // Because the tree we're interested in contains only one operator type, and // all eligible operators are left-associative, the shape of the tree is // very restricted: it's a linked list along the left edges. // This simplifies our implementation. const SelectionTree::Node *Start = Op.SelectedOperands.front(); // LHS const SelectionTree::Node *End = Op.SelectedOperands.back(); // RHS // End is already correct: it can't be an OuterOp (as it's left-associative). // Start needs to be pushed down int the subtree to the right spot. while (Op.parse(Start->ignoreImplicit()) && Op.Kind == OuterOp && !Op.crossesMacroBoundary(SM)) { assert(!Op.SelectedOperands.empty() && "got only operator on one side!"); if (Op.SelectedOperands.size() == 1) { // Only Op.RHS selected Start = Op.SelectedOperands.back(); break; } // Op.LHS is (at least partially) selected, so descend into it. Start = Op.SelectedOperands.front(); } return SourceRange( toHalfOpenFileRange(SM, LangOpts, Start->ASTNode.getSourceRange()) ->getBegin(), toHalfOpenFileRange(SM, LangOpts, End->ASTNode.getSourceRange()) ->getEnd()); } SourceRange ExtractionContext::getExtractionChars() const { // Special case: we're extracting an associative binary subexpression. SourceRange BinaryOperatorRange = getBinaryOperatorRange(*ExprNode, SM, Ctx.getLangOpts()); if (BinaryOperatorRange.isValid()) return BinaryOperatorRange; // Usual case: we're extracting the whole expression. return *toHalfOpenFileRange(SM, Ctx.getLangOpts(), Expr->getSourceRange()); } // Find the CallExpr whose callee is the (possibly wrapped) DeclRef const SelectionTree::Node *getCallExpr(const SelectionTree::Node *DeclRef) { const SelectionTree::Node &MaybeCallee = DeclRef->outerImplicit(); const SelectionTree::Node *MaybeCall = MaybeCallee.Parent; if (!MaybeCall) return nullptr; const CallExpr *CE = llvm::dyn_cast_or_null(MaybeCall->ASTNode.get()); if (!CE) return nullptr; if (CE->getCallee() != MaybeCallee.ASTNode.get()) return nullptr; return MaybeCall; } // Returns true if Inner (which is a direct child of Outer) is appearing as // a statement rather than an expression whose value can be used. bool childExprIsStmt(const Stmt *Outer, const Expr *Inner) { if (!Outer || !Inner) return false; // Exclude the most common places where an expr can appear but be unused. if (llvm::isa(Outer)) return true; if (llvm::isa(Outer)) return true; // Control flow statements use condition etc, but not the body. if (const auto* WS = llvm::dyn_cast(Outer)) return Inner == WS->getBody(); if (const auto* DS = llvm::dyn_cast(Outer)) return Inner == DS->getBody(); if (const auto* FS = llvm::dyn_cast(Outer)) return Inner == FS->getBody(); if (const auto* FS = llvm::dyn_cast(Outer)) return Inner == FS->getBody(); if (const auto* IS = llvm::dyn_cast(Outer)) return Inner == IS->getThen() || Inner == IS->getElse(); // Assume all other cases may be actual expressions. // This includes the important case of subexpressions (where Outer is Expr). return false; } // check if N can and should be extracted (e.g. is not void-typed). bool eligibleForExtraction(const SelectionTree::Node *N) { const Expr *E = N->ASTNode.get(); if (!E) return false; // Void expressions can't be assigned to variables. if (const Type *ExprType = E->getType().getTypePtrOrNull()) if (ExprType->isVoidType()) return false; // A plain reference to a name (e.g. variable) isn't worth extracting. // FIXME: really? What if it's e.g. `std::is_same::value`? if (llvm::isa(E) || llvm::isa(E)) return false; // Extracting Exprs like a = 1 gives dummy = a = 1 which isn't useful. // FIXME: we could still hoist the assignment, and leave the variable there? ParsedBinaryOperator BinOp; if (BinOp.parse(*N) && BinaryOperator::isAssignmentOp(BinOp.Kind)) return false; // We don't want to extract expressions used as statements, that would leave // a `dummy;` around that has no effect. // Unfortunately because the AST doesn't have ExprStmt, we have to check in // this roundabout way. const SelectionTree::Node &OuterImplicit = N->outerImplicit(); if (!OuterImplicit.Parent || childExprIsStmt(OuterImplicit.Parent->ASTNode.get(), OuterImplicit.ASTNode.get())) return false; // FIXME: ban extracting the RHS of an assignment: `a = [[foo()]]` return true; } // Find the Expr node that we're going to extract. // We don't want to trigger for assignment expressions and variable/field // DeclRefs. For function/member function, we want to extract the entire // function call. const SelectionTree::Node *computeExtractedExpr(const SelectionTree::Node *N) { if (!N) return nullptr; const SelectionTree::Node *TargetNode = N; const clang::Expr *SelectedExpr = N->ASTNode.get(); if (!SelectedExpr) return nullptr; // For function and member function DeclRefs, extract the whole call. if (llvm::isa(SelectedExpr) || llvm::isa(SelectedExpr)) if (const SelectionTree::Node *Call = getCallExpr(N)) TargetNode = Call; // Extracting Exprs like a = 1 gives dummy = a = 1 which isn't useful. if (const BinaryOperator *BinOpExpr = dyn_cast_or_null(SelectedExpr)) { if (BinOpExpr->getOpcode() == BinaryOperatorKind::BO_Assign) return nullptr; } if (!TargetNode || !eligibleForExtraction(TargetNode)) return nullptr; return TargetNode; } /// Extracts an expression to the variable dummy /// Before: /// int x = 5 + 4 * 3; /// ^^^^^ /// After: /// auto dummy = 5 + 4; /// int x = dummy * 3; class ExtractVariable : public Tweak { public: const char *id() const override final; bool prepare(const Selection &Inputs) override; Expected apply(const Selection &Inputs) override; std::string title() const override { return "Extract subexpression to variable"; } - Intent intent() const override { return Refactor; } + llvm::StringLiteral kind() const override { + return CodeAction::REFACTOR_KIND; + } private: // the expression to extract std::unique_ptr Target; }; REGISTER_TWEAK(ExtractVariable) bool ExtractVariable::prepare(const Selection &Inputs) { // we don't trigger on empty selections for now if (Inputs.SelectionBegin == Inputs.SelectionEnd) return false; const ASTContext &Ctx = Inputs.AST->getASTContext(); // FIXME: Enable non-C++ cases once we start spelling types explicitly instead // of making use of auto. if (!Ctx.getLangOpts().CPlusPlus) return false; const SourceManager &SM = Inputs.AST->getSourceManager(); if (const SelectionTree::Node *N = computeExtractedExpr(Inputs.ASTSelection.commonAncestor())) Target = std::make_unique(N, SM, Ctx); return Target && Target->isExtractable(); } Expected ExtractVariable::apply(const Selection &Inputs) { tooling::Replacements Result; // FIXME: get variable name from user or suggest based on type std::string VarName = "dummy"; SourceRange Range = Target->getExtractionChars(); // insert new variable declaration if (auto Err = Result.add(Target->insertDeclaration(VarName, Range))) return std::move(Err); // replace expression with variable name if (auto Err = Result.add(Target->replaceWithVar(Range, VarName))) return std::move(Err); return Effect::mainFileEdit(Inputs.AST->getSourceManager(), std::move(Result)); } } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/refactor/tweaks/ObjCLocalizeStringLiteral.cpp b/clang-tools-extra/clangd/refactor/tweaks/ObjCLocalizeStringLiteral.cpp index 894f018aa796..0c50db79d367 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/ObjCLocalizeStringLiteral.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/ObjCLocalizeStringLiteral.cpp @@ -1,88 +1,90 @@ //===--- ObjcLocalizeStringLiteral.cpp ---------------------------*- C++-*-===// // // 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 "ParsedAST.h" #include "SourceCode.h" #include "refactor/Tweak.h" #include "support/Logger.h" #include "clang/AST/ExprObjC.h" #include "clang/Basic/LangOptions.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/None.h" #include "llvm/ADT/Optional.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/iterator_range.h" #include "llvm/Support/Casting.h" #include "llvm/Support/Error.h" namespace clang { namespace clangd { namespace { /// Wraps an Objective-C string literal with the NSLocalizedString macro. /// Before: /// @"description" /// ^^^ /// After: /// NSLocalizedString(@"description", @"") class ObjCLocalizeStringLiteral : public Tweak { public: const char *id() const override final; - Intent intent() const override { return Intent::Refactor; } + llvm::StringLiteral kind() const override { + return CodeAction::REFACTOR_KIND; + } bool prepare(const Selection &Inputs) override; Expected apply(const Selection &Inputs) override; std::string title() const override; private: const clang::ObjCStringLiteral *Str = nullptr; }; REGISTER_TWEAK(ObjCLocalizeStringLiteral) bool ObjCLocalizeStringLiteral::prepare(const Selection &Inputs) { const SelectionTree::Node *N = Inputs.ASTSelection.commonAncestor(); if (!N) return false; // Allow the refactoring even if the user selected only the C string part // of the expression. if (N->ASTNode.get()) { if (N->Parent) N = N->Parent; } Str = dyn_cast_or_null(N->ASTNode.get()); return Str; } Expected ObjCLocalizeStringLiteral::apply(const Selection &Inputs) { auto *AST = Inputs.AST; auto &SM = AST->getSourceManager(); const auto &TB = AST->getTokens(); auto Toks = TB.spelledForExpanded(TB.expandedTokens(Str->getSourceRange())); if (!Toks || Toks->empty()) return error("Failed to find tokens to replace."); // Insert `NSLocalizedString(` before the literal. auto Reps = tooling::Replacements(tooling::Replacement( SM, Toks->front().location(), 0, "NSLocalizedString(")); // Insert `, @"")` after the literal. if (auto Err = Reps.add( tooling::Replacement(SM, Toks->back().endLocation(), 0, ", @\"\")"))) return std::move(Err); return Effect::mainFileEdit(SM, std::move(Reps)); } std::string ObjCLocalizeStringLiteral::title() const { return "Wrap in NSLocalizedString"; } } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/refactor/tweaks/PopulateSwitch.cpp b/clang-tools-extra/clangd/refactor/tweaks/PopulateSwitch.cpp index 753e8b4df826..12a6e49a1684 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/PopulateSwitch.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/PopulateSwitch.cpp @@ -1,189 +1,191 @@ //===--- PopulateSwitch.cpp --------------------------------------*- C++-*-===// // // 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 // //===----------------------------------------------------------------------===// // // Tweak that populates an empty switch statement of an enumeration type with // all of the enumerators of that type. // // Before: // enum Color { RED, GREEN, BLUE }; // // void f(Color color) { // switch (color) {} // } // // After: // enum Color { RED, GREEN, BLUE }; // // void f(Color color) { // switch (color) { // case RED: // case GREEN: // case BLUE: // break; // } // } // //===----------------------------------------------------------------------===// #include "AST.h" #include "Selection.h" #include "refactor/Tweak.h" #include "support/Logger.h" #include "clang/AST/Decl.h" #include "clang/AST/Stmt.h" #include "clang/AST/Type.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/SmallSet.h" #include #include namespace clang { namespace clangd { namespace { class PopulateSwitch : public Tweak { const char *id() const override; bool prepare(const Selection &Sel) override; Expected apply(const Selection &Sel) override; std::string title() const override { return "Populate switch"; } - Intent intent() const override { return Refactor; } + llvm::StringLiteral kind() const override { + return CodeAction::REFACTOR_KIND; + } private: const DeclContext *DeclCtx = nullptr; const SwitchStmt *Switch = nullptr; const CompoundStmt *Body = nullptr; const EnumType *EnumT = nullptr; const EnumDecl *EnumD = nullptr; }; REGISTER_TWEAK(PopulateSwitch) bool PopulateSwitch::prepare(const Selection &Sel) { const SelectionTree::Node *CA = Sel.ASTSelection.commonAncestor(); if (!CA) return false; const Stmt *CAStmt = CA->ASTNode.get(); if (!CAStmt) return false; // Go up a level if we see a compound statement. // switch (value) {} // ^^ if (isa(CAStmt)) { CA = CA->Parent; if (!CA) return false; CAStmt = CA->ASTNode.get(); if (!CAStmt) return false; } DeclCtx = &CA->getDeclContext(); Switch = dyn_cast(CAStmt); if (!Switch) return false; Body = dyn_cast(Switch->getBody()); if (!Body) return false; const Expr *Cond = Switch->getCond(); if (!Cond) return false; // Ignore implicit casts, since enums implicitly cast to integer types. Cond = Cond->IgnoreParenImpCasts(); EnumT = Cond->getType()->getAsAdjusted(); if (!EnumT) return false; EnumD = EnumT->getDecl(); if (!EnumD) return false; // We trigger if there are fewer cases than enum values (and no case covers // multiple values). This guarantees we'll have at least one case to insert. // We don't yet determine what the cases are, as that means evaluating // expressions. auto I = EnumD->enumerator_begin(); auto E = EnumD->enumerator_end(); for (const SwitchCase *CaseList = Switch->getSwitchCaseList(); CaseList && I != E; CaseList = CaseList->getNextSwitchCase(), I++) { // Default likely intends to cover cases we'd insert. if (isa(CaseList)) return false; const CaseStmt *CS = cast(CaseList); // Case statement covers multiple values, so just counting doesn't work. if (CS->caseStmtIsGNURange()) return false; // Case expression is not a constant expression or is value-dependent, // so we may not be able to work out which cases are covered. const ConstantExpr *CE = dyn_cast(CS->getLHS()); if (!CE || CE->isValueDependent()) return false; } // Only suggest tweak if we have more enumerators than cases. return I != E; } Expected PopulateSwitch::apply(const Selection &Sel) { ASTContext &Ctx = Sel.AST->getASTContext(); // Get the enum's integer width and signedness, for adjusting case literals. unsigned EnumIntWidth = Ctx.getIntWidth(QualType(EnumT, 0)); bool EnumIsSigned = EnumT->isSignedIntegerOrEnumerationType(); llvm::SmallSet ExistingEnumerators; for (const SwitchCase *CaseList = Switch->getSwitchCaseList(); CaseList; CaseList = CaseList->getNextSwitchCase()) { const CaseStmt *CS = cast(CaseList); assert(!CS->caseStmtIsGNURange()); const ConstantExpr *CE = cast(CS->getLHS()); assert(!CE->isValueDependent()); llvm::APSInt Val = CE->getResultAsAPSInt(); Val = Val.extOrTrunc(EnumIntWidth); Val.setIsSigned(EnumIsSigned); ExistingEnumerators.insert(Val); } SourceLocation Loc = Body->getRBracLoc(); ASTContext &DeclASTCtx = DeclCtx->getParentASTContext(); std::string Text; for (EnumConstantDecl *Enumerator : EnumD->enumerators()) { if (ExistingEnumerators.contains(Enumerator->getInitVal())) continue; Text += "case "; Text += getQualification(DeclASTCtx, DeclCtx, Loc, EnumD); if (EnumD->isScoped()) { Text += EnumD->getName(); Text += "::"; } Text += Enumerator->getName(); Text += ":"; } assert(!Text.empty() && "No enumerators to insert!"); Text += "break;"; const SourceManager &SM = Ctx.getSourceManager(); return Effect::mainFileEdit( SM, tooling::Replacements(tooling::Replacement(SM, Loc, 0, Text))); } } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/refactor/tweaks/RawStringLiteral.cpp b/clang-tools-extra/clangd/refactor/tweaks/RawStringLiteral.cpp index bec45be6c325..b0ab3067449b 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/RawStringLiteral.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/RawStringLiteral.cpp @@ -1,100 +1,102 @@ //===--- RawStringLiteral.cpp ------------------------------------*- C++-*-===// // // 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 "ParsedAST.h" #include "SourceCode.h" #include "refactor/Tweak.h" #include "support/Logger.h" #include "clang/AST/ASTContext.h" #include "clang/AST/RecursiveASTVisitor.h" #include "clang/AST/Stmt.h" #include "clang/Basic/LangOptions.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" #include "clang/Lex/Lexer.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/None.h" #include "llvm/ADT/Optional.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/iterator_range.h" #include "llvm/Support/Casting.h" #include "llvm/Support/Error.h" namespace clang { namespace clangd { namespace { /// Converts a string literal to a raw string. /// Before: /// printf("\"a\"\nb"); /// ^^^^^^^^^ /// After: /// printf(R"("a" /// b)"); class RawStringLiteral : public Tweak { public: const char *id() const override final; bool prepare(const Selection &Inputs) override; Expected apply(const Selection &Inputs) override; std::string title() const override { return "Convert to raw string"; } - Intent intent() const override { return Refactor; } + llvm::StringLiteral kind() const override { + return CodeAction::REFACTOR_KIND; + } private: const clang::StringLiteral *Str = nullptr; }; REGISTER_TWEAK(RawStringLiteral) static bool isNormalString(const StringLiteral &Str, SourceLocation Cursor, SourceManager &SM) { // All chunks must be normal ASCII strings, not u8"..." etc. if (!Str.isAscii()) return false; SourceLocation LastTokenBeforeCursor; for (auto I = Str.tokloc_begin(), E = Str.tokloc_end(); I != E; ++I) { if (I->isMacroID()) // No tokens in the string may be macro expansions. return false; if (SM.isBeforeInTranslationUnit(*I, Cursor) || *I == Cursor) LastTokenBeforeCursor = *I; } // Token we care about must be a normal "string": not raw, u8, etc. const char* Data = SM.getCharacterData(LastTokenBeforeCursor); return Data && *Data == '"'; } static bool needsRaw(llvm::StringRef Content) { return Content.find_first_of("\"\n\t") != StringRef::npos; } static bool canBeRaw(llvm::StringRef Content) { for (char C : Content) if (!llvm::isPrint(C) && C != '\n' && C != '\t') return false; return !Content.contains(")\""); } bool RawStringLiteral::prepare(const Selection &Inputs) { const SelectionTree::Node *N = Inputs.ASTSelection.commonAncestor(); if (!N) return false; Str = dyn_cast_or_null(N->ASTNode.get()); return Str && isNormalString(*Str, Inputs.Cursor, Inputs.AST->getSourceManager()) && needsRaw(Str->getBytes()) && canBeRaw(Str->getBytes()); } Expected RawStringLiteral::apply(const Selection &Inputs) { auto &SM = Inputs.AST->getSourceManager(); auto Reps = tooling::Replacements( tooling::Replacement(SM, Str, ("R\"(" + Str->getBytes() + ")\"").str(), Inputs.AST->getLangOpts())); return Effect::mainFileEdit(SM, std::move(Reps)); } } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/refactor/tweaks/RemoveUsingNamespace.cpp b/clang-tools-extra/clangd/refactor/tweaks/RemoveUsingNamespace.cpp index 9d1a9f12567c..8bd9703397b6 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/RemoveUsingNamespace.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/RemoveUsingNamespace.cpp @@ -1,207 +1,209 @@ //===--- RemoveUsingNamespace.cpp --------------------------------*- C++-*-===// // // 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 "AST.h" #include "FindTarget.h" #include "Selection.h" #include "SourceCode.h" #include "refactor/Tweak.h" #include "support/Logger.h" #include "clang/AST/Decl.h" #include "clang/AST/DeclBase.h" #include "clang/AST/DeclCXX.h" #include "clang/AST/RecursiveASTVisitor.h" #include "clang/Basic/SourceLocation.h" #include "clang/Tooling/Core/Replacement.h" #include "clang/Tooling/Refactoring/RecursiveSymbolVisitor.h" #include "llvm/ADT/ScopeExit.h" namespace clang { namespace clangd { namespace { /// Removes the 'using namespace' under the cursor and qualifies all accesses in /// the current file. E.g., /// using namespace std; /// vector foo(std::map); /// Would become: /// std::vector foo(std::map); /// Currently limited to using namespace directives inside global namespace to /// simplify implementation. Also the namespace must not contain using /// directives. class RemoveUsingNamespace : public Tweak { public: const char *id() const override; bool prepare(const Selection &Inputs) override; Expected apply(const Selection &Inputs) override; std::string title() const override; - Intent intent() const override { return Refactor; } + llvm::StringLiteral kind() const override { + return CodeAction::REFACTOR_KIND; + } private: const UsingDirectiveDecl *TargetDirective = nullptr; }; REGISTER_TWEAK(RemoveUsingNamespace) class FindSameUsings : public RecursiveASTVisitor { public: FindSameUsings(const UsingDirectiveDecl &Target, std::vector &Results) : TargetNS(Target.getNominatedNamespace()), TargetCtx(Target.getDeclContext()), Results(Results) {} bool VisitUsingDirectiveDecl(UsingDirectiveDecl *D) { if (D->getNominatedNamespace() != TargetNS || D->getDeclContext() != TargetCtx) return true; Results.push_back(D); return true; } private: const NamespaceDecl *TargetNS; const DeclContext *TargetCtx; std::vector &Results; }; /// Produce edit removing 'using namespace xxx::yyy' and the trailing semicolon. llvm::Expected removeUsingDirective(ASTContext &Ctx, const UsingDirectiveDecl *D) { auto &SM = Ctx.getSourceManager(); llvm::Optional NextTok = Lexer::findNextToken(D->getEndLoc(), SM, Ctx.getLangOpts()); if (!NextTok || NextTok->isNot(tok::semi)) return error("no semicolon after using-directive"); // FIXME: removing the semicolon may be invalid in some obscure cases, e.g. // if (x) using namespace std; else using namespace bar; return tooling::Replacement( SM, CharSourceRange::getTokenRange(D->getBeginLoc(), NextTok->getLocation()), "", Ctx.getLangOpts()); } // Returns true iff the parent of the Node is a TUDecl. bool isTopLevelDecl(const SelectionTree::Node *Node) { return Node->Parent && Node->Parent->ASTNode.get(); } // Returns the first visible context that contains this DeclContext. // For example: Returns ns1 for S1 and a. // namespace ns1 { // inline namespace ns2 { struct S1 {}; } // enum E { a, b, c, d }; // } const DeclContext *visibleContext(const DeclContext *D) { while (D->isInlineNamespace() || D->isTransparentContext()) D = D->getParent(); return D; } bool RemoveUsingNamespace::prepare(const Selection &Inputs) { // Find the 'using namespace' directive under the cursor. auto *CA = Inputs.ASTSelection.commonAncestor(); if (!CA) return false; TargetDirective = CA->ASTNode.get(); if (!TargetDirective) return false; if (!dyn_cast(TargetDirective->getDeclContext())) return false; // FIXME: Unavailable for namespaces containing using-namespace decl. // It is non-trivial to deal with cases where identifiers come from the inner // namespace. For example map has to be changed to aa::map. // namespace aa { // namespace bb { struct map {}; } // using namespace bb; // } // using namespace a^a; // int main() { map m; } // We need to make this aware of the transitive using-namespace decls. if (!TargetDirective->getNominatedNamespace()->using_directives().empty()) return false; return isTopLevelDecl(CA); } Expected RemoveUsingNamespace::apply(const Selection &Inputs) { auto &Ctx = Inputs.AST->getASTContext(); auto &SM = Ctx.getSourceManager(); // First, collect *all* using namespace directives that redeclare the same // namespace. std::vector AllDirectives; FindSameUsings(*TargetDirective, AllDirectives).TraverseAST(Ctx); SourceLocation FirstUsingDirectiveLoc; for (auto *D : AllDirectives) { if (FirstUsingDirectiveLoc.isInvalid() || SM.isBeforeInTranslationUnit(D->getBeginLoc(), FirstUsingDirectiveLoc)) FirstUsingDirectiveLoc = D->getBeginLoc(); } // Collect all references to symbols from the namespace for which we're // removing the directive. std::vector IdentsToQualify; for (auto &D : Inputs.AST->getLocalTopLevelDecls()) { findExplicitReferences(D, [&](ReferenceLoc Ref) { if (Ref.Qualifier) return; // This reference is already qualified. for (auto *T : Ref.Targets) { if (!visibleContext(T->getDeclContext()) ->Equals(TargetDirective->getNominatedNamespace())) return; } SourceLocation Loc = Ref.NameLoc; if (Loc.isMacroID()) { // Avoid adding qualifiers before macro expansions, it's probably // incorrect, e.g. // namespace std { int foo(); } // #define FOO 1 + foo() // using namespace foo; // provides matrix // auto x = FOO; // Must not changed to auto x = std::FOO if (!SM.isMacroArgExpansion(Loc)) return; // FIXME: report a warning to the users. Loc = SM.getFileLoc(Ref.NameLoc); } assert(Loc.isFileID()); if (SM.getFileID(Loc) != SM.getMainFileID()) return; // FIXME: report these to the user as warnings? if (SM.isBeforeInTranslationUnit(Loc, FirstUsingDirectiveLoc)) return; // Directive was not visible before this point. IdentsToQualify.push_back(Loc); }); } // Remove duplicates. llvm::sort(IdentsToQualify); IdentsToQualify.erase( std::unique(IdentsToQualify.begin(), IdentsToQualify.end()), IdentsToQualify.end()); // Produce replacements to remove the using directives. tooling::Replacements R; for (auto *D : AllDirectives) { auto RemoveUsing = removeUsingDirective(Ctx, D); if (!RemoveUsing) return RemoveUsing.takeError(); if (auto Err = R.add(*RemoveUsing)) return std::move(Err); } // Produce replacements to add the qualifiers. std::string Qualifier = printUsingNamespaceName(Ctx, *TargetDirective) + "::"; for (auto Loc : IdentsToQualify) { if (auto Err = R.add(tooling::Replacement(Ctx.getSourceManager(), Loc, /*Length=*/0, Qualifier))) return std::move(Err); } return Effect::mainFileEdit(SM, std::move(R)); } std::string RemoveUsingNamespace::title() const { return std::string( llvm::formatv("Remove using namespace, re-qualify names instead.")); } } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/refactor/tweaks/SwapIfBranches.cpp b/clang-tools-extra/clangd/refactor/tweaks/SwapIfBranches.cpp index d5299f014cc7..976f68215581 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/SwapIfBranches.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/SwapIfBranches.cpp @@ -1,95 +1,97 @@ //===--- SwapIfBranches.cpp --------------------------------------*- C++-*-===// // // 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 "ParsedAST.h" #include "SourceCode.h" #include "refactor/Tweak.h" #include "support/Logger.h" #include "clang/AST/ASTContext.h" #include "clang/AST/RecursiveASTVisitor.h" #include "clang/AST/Stmt.h" #include "clang/Basic/LangOptions.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" #include "clang/Lex/Lexer.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/None.h" #include "llvm/ADT/Optional.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Casting.h" #include "llvm/Support/Error.h" namespace clang { namespace clangd { namespace { /// Swaps the 'then' and 'else' branch of the if statement. /// Before: /// if (foo) { return 10; } else { continue; } /// ^^^^^^^ ^^^^ /// After: /// if (foo) { continue; } else { return 10; } class SwapIfBranches : public Tweak { public: const char *id() const override final; bool prepare(const Selection &Inputs) override; Expected apply(const Selection &Inputs) override; std::string title() const override { return "Swap if branches"; } - Intent intent() const override { return Refactor; } + llvm::StringLiteral kind() const override { + return CodeAction::REFACTOR_KIND; + } bool hidden() const override { return true; } private: const IfStmt *If = nullptr; }; REGISTER_TWEAK(SwapIfBranches) bool SwapIfBranches::prepare(const Selection &Inputs) { for (const SelectionTree::Node *N = Inputs.ASTSelection.commonAncestor(); N && !If; N = N->Parent) { // Stop once we hit a block, e.g. a lambda in the if condition. if (dyn_cast_or_null(N->ASTNode.get())) return false; If = dyn_cast_or_null(N->ASTNode.get()); } // avoid dealing with single-statement brances, they require careful handling // to avoid changing semantics of the code (i.e. dangling else). return If && dyn_cast_or_null(If->getThen()) && dyn_cast_or_null(If->getElse()); } Expected SwapIfBranches::apply(const Selection &Inputs) { auto &Ctx = Inputs.AST->getASTContext(); auto &SrcMgr = Inputs.AST->getSourceManager(); auto ThenRng = toHalfOpenFileRange(SrcMgr, Ctx.getLangOpts(), If->getThen()->getSourceRange()); if (!ThenRng) return error("Could not obtain range of the 'then' branch. Macros?"); auto ElseRng = toHalfOpenFileRange(SrcMgr, Ctx.getLangOpts(), If->getElse()->getSourceRange()); if (!ElseRng) return error("Could not obtain range of the 'else' branch. Macros?"); auto ThenCode = toSourceCode(SrcMgr, *ThenRng); auto ElseCode = toSourceCode(SrcMgr, *ElseRng); tooling::Replacements Result; if (auto Err = Result.add(tooling::Replacement(Ctx.getSourceManager(), ThenRng->getBegin(), ThenCode.size(), ElseCode))) return std::move(Err); if (auto Err = Result.add(tooling::Replacement(Ctx.getSourceManager(), ElseRng->getBegin(), ElseCode.size(), ThenCode))) return std::move(Err); return Effect::mainFileEdit(SrcMgr, std::move(Result)); } } // namespace } // namespace clangd } // namespace clang