diff --git a/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt b/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt --- a/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt +++ b/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt @@ -21,6 +21,7 @@ ExpandMacro.cpp ExtractFunction.cpp ExtractVariable.cpp + ImplementAbstract.cpp ObjCLocalizeStringLiteral.cpp PopulateSwitch.cpp RawStringLiteral.cpp diff --git a/clang-tools-extra/clangd/refactor/tweaks/ImplementAbstract.cpp b/clang-tools-extra/clangd/refactor/tweaks/ImplementAbstract.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/refactor/tweaks/ImplementAbstract.cpp @@ -0,0 +1,421 @@ +//===--- ImplementAbstract.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 "support/Logger.h" +#include "clang/Basic/Specifiers.h" +#include "llvm/ADT/PointerIntPair.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/Error.h" + +namespace clang { +namespace clangd { + +namespace { + +// FIXME: Have some way to control this, maybe in the config? +constexpr bool DefineMethods = true; + +using MethodAndAccess = + llvm::PointerIntPair; + +AccessSpecifier getMostConstrained(AccessSpecifier InheritSpecifier, + AccessSpecifier DefinedAs) { + return std::max(InheritSpecifier, DefinedAs); +} + +/// Stores all pure methods in \p Record that aren't in \p Overrides in \p +/// Results. The methods are stored the most constrained access of \p Access and +/// the AccessSpecifier of the method. +void collectNonOverriddenPureMethods( + const CXXRecordDecl &Record, + llvm::SmallVectorImpl &Results, AccessSpecifier Access, + const llvm::SmallPtrSetImpl &Overrides) { + for (const CXXMethodDecl *Method : Record.methods()) { + if (!Method->isPure()) + continue; + if (!Overrides.contains(Method)) + Results.emplace_back(Method, + getMostConstrained(Access, Method->getAccess())); + } +} + +/// Populates \p Overrides with all the methods that are overridden by methods +/// in \p Record. If \p IsRoot is true and there are any pure methods in \p +/// Record, return true, otherwise return false. +bool buildOverrideSet(const CXXRecordDecl &Record, + llvm::SmallPtrSetImpl &Overrides, + bool IsRoot) { + for (const CXXMethodDecl *Method : Record.methods()) { + if (!Method->isVirtual()) + continue; + if (IsRoot && Method->isPure()) + return true; + for (const auto *Overriding : Method->overridden_methods()) + Overrides.insert(Overriding); + } + return false; +} + +/// Collect all the pure virtual methods in \p Record and its base classes that +/// don't appear in \p Overrides, store the results in \p Results. Returns true +/// if any of the bases are dependent, otherwise false. +bool collectPureMethodsImpl( + const CXXRecordDecl &Record, + llvm::SmallVectorImpl &Results, AccessSpecifier Access, + llvm::SmallPtrSetImpl &Overrides) { + if (Record.getNumBases() > 0) { + buildOverrideSet(Record, Overrides, false); + for (const CXXBaseSpecifier &Base : Record.bases()) { + const RecordType *RT = Base.getType()->getAs(); + if (!RT) + // Probably a dependent base, just error out. + return true; + const CXXRecordDecl *BaseDecl = cast(RT->getDecl()); + if (!BaseDecl->isPolymorphic()) + continue; + if (collectPureMethodsImpl( + *BaseDecl, Results, + getMostConstrained(Access, Base.getAccessSpecifier()), Overrides)) + // Propergate any error back up. + return true; + } + } + // Add the Pure methods from this class after traversing the bases. This means + // when it comes time to create implementation, methods from classes higher up + // the heirachy will appear first. + collectNonOverriddenPureMethods(Record, Results, Access, Overrides); + return false; +} + +/// Collect all the pure virtual methods from the base class \p Base that +/// haven't been overridden in \p Record. Store the results in \p Results. +bool collectPureMethodsFromBase( + const CXXRecordDecl &RD, const CXXBaseSpecifier &Base, + llvm::SmallVectorImpl &Results) { + assert(llvm::any_of(RD.bases(), [&Base](const CXXBaseSpecifier &Base2) { + // CXXBaseSpecifier has no operator== and as DynTypedNode holds a copy, we + // can't use pointer identity. This check should ensure the base we have + // selected comes from RD. + return Base.getTypeSourceInfo() == Base2.getTypeSourceInfo() && + Base.getSourceRange() == Base2.getSourceRange(); + })); + const RecordType *RT = Base.getType()->getAs(); + if (!RT) + // Probably a dependent base, just error out. + return true; + const CXXRecordDecl *BaseDecl = cast(RT->getDecl()); + if (!BaseDecl->isPolymorphic()) + return true; + llvm::SmallPtrSet Overrides; + if (buildOverrideSet(RD, Overrides, true)) + return true; + return collectPureMethodsImpl(*BaseDecl, Results, Base.getAccessSpecifier(), + Overrides); +} + +bool collectAllPureMethods(const CXXRecordDecl &RD, + llvm::SmallVectorImpl &Results) { + llvm::SmallPtrSet Overrides; + buildOverrideSet(RD, Overrides, true); + return collectPureMethodsImpl(RD, Results, AS_public, Overrides); +} + +/// Gets the class at the Selection \p Inputs. If the selection is in +/// the base-specifier-list, The base that it's over will be stored in \p +/// BaseSpec. \returns nullptr if no class could be found. +const CXXRecordDecl *getSelectedRecord(const Tweak::Selection &Inputs, + Optional *BaseSpec) { + const SelectionTree::Node *Node = Inputs.ASTSelection.commonAncestor(); + if (!Node) + return nullptr; + if (const auto *RD = Node->ASTNode.get()) + return RD; + // Handle when the selection is over the base specifier. This is recursive + // because the base specifier could be a template instantiation containing + // multiple nodes. + for (; Node->Parent; Node = Node->Parent) { + if (const auto *BS = Node->ASTNode.get()) { + if (const auto *RD = Node->Parent->ASTNode.get()) { + if (BaseSpec) + *BaseSpec = *BS; + return RD; + } + } + } + return nullptr; +} + +/// Some quick to check basic heuristics to check before we try and collect +/// virtual methods. +bool isClassOK(const CXXRecordDecl &RecordDecl) { + if (!RecordDecl.isThisDeclarationADefinition()) + return false; + if (!RecordDecl.isClass() && !RecordDecl.isStruct()) + return false; + if (RecordDecl.hasAnyDependentBases() || RecordDecl.getNumBases() == 0) + return false; + // We should check for abstract, but that prevents working on template classes + // that don't have any dependent bases. + if (!RecordDecl.isPolymorphic()) + return false; + return true; +} + +struct InsertionDetail { + SourceLocation Loc = {}; + AccessSpecifier Access; + unsigned char AfterPriority = 0; +}; + +// This is a little hacky because EndLoc of a decl doesn't include +// the semi-colon. +auto getLocAfterDecl(const Decl &D, const SourceManager &SM, + const LangOptions &LO) { + if (D.hasBody()) + return D.getEndLoc().getLocWithOffset(1); + if (auto Next = Lexer::findNextToken(D.getEndLoc(), SM, LO)) { + if (Next->is(tok::semi)) + return Next->getEndLoc(); + } + return D.getEndLoc().getLocWithOffset(1); +} + +/// Generate insertion points in \p R that don't require inserting access +/// specifiers. The insertion points generally try to appear after the last +/// method declared in the class with a specific access. \p ShouldIncludeAccess +/// is a way to avoid generating insertion points for access specifiers we +/// aren't going to fill in. +SmallVector +getInsertionPoints(const CXXRecordDecl &R, ArrayRef ShouldIncludeAccess, + const SourceManager &SM, const LangOptions &LO) { + SmallVector Result; + auto GetDetailForAccess = [&](AccessSpecifier Spec) -> InsertionDetail & { + assert(Spec != AS_none); + for (InsertionDetail &Item : Result) { + if (Item.Access == Spec) + return Item; + } + return Result.emplace_back(InsertionDetail{{}, Spec}); + }; + + // This whole block is designed to get an insertion point after the last + // method has been declared with each access specifier. Doing this ensures we + // keep the same visibility for implemented methods without the need to add + // unnecessary access specifiers. + for (auto *Decl : R.decls()) { + if (!ShouldIncludeAccess[Decl->getAccess()]) + continue; + // Ignore things like compiler generated special member functions. + if (Decl->isImplicit()) + continue; + // Hack to try and leave the destructor as last method in a block. + if (isa(Decl)) + continue; + InsertionDetail &Detail = GetDetailForAccess(Decl->getAccess()); + if (isa(Decl)) { + Detail.Loc = getLocAfterDecl(*Decl, SM, LO); + Detail.AfterPriority = 2; + } else { + // Try to put methods after access spec but before fields. + auto Priority = isa(Decl) ? 1 : 0; + if (Detail.AfterPriority <= Priority) { + Detail.Loc = getLocAfterDecl(*Decl, SM, LO); + Detail.AfterPriority = Priority; + } + } + } + if (Result.empty()) { + auto Access = R.isClass() ? AS_private : AS_public; + if (ShouldIncludeAccess[Access]) { + // An empty class so start inserting methods that don't need an access + // specifier just after the open curly brace. + GetDetailForAccess(Access).Loc = + R.getBraceRange().getBegin().getLocWithOffset(1); + } + } + return Result; +} + +class PrintingInContextCallback : public PrintingCallbacks { +public: + PrintingInContextCallback(const DeclContext *CurContext) + : CurContext(CurContext) {} + virtual ~PrintingInContextCallback() = default; + bool isScopeVisible(const DeclContext *DC) const override { + return DC->Encloses(CurContext); + } + +private: + const DeclContext *CurContext; +}; + +void printMethods(llvm::raw_ostream &Out, ArrayRef Items, + AccessSpecifier AccessKind, const PrintingPolicy &Policy, + bool PrintAccessSpec, bool CreateBody) { + if (PrintAccessSpec) + Out << "\n" << getAccessSpelling(AccessKind) << ":\n"; + Out << "\n"; + for (const auto &MethodAndAccess : Items) { + if (MethodAndAccess.getInt() != AccessKind) + continue; + const CXXMethodDecl *Method = MethodAndAccess.getPointer(); + Method->getReturnType().print(Out, Policy); + Out << ' '; + Out << Method->getNameAsString() << "("; + bool IsFirst = true; + for (const auto &Param : Method->parameters()) { + if (!IsFirst) + Out << ", "; + else + IsFirst = false; + Param->print(Out, Policy); + } + Out << ") "; + if (Method->isConst()) + Out << "const "; + if (Method->isVolatile()) + Out << "volatile "; + if (CreateBody) { + Out << "override {\n"; + if (!Method->getReturnType()->isVoidType()) + Out << "return {};\n"; + Out << "}\n"; + } else { + Out << "override;\n"; + } + } +} + +class ImplementAbstract : public Tweak { +public: + const char *id() const override; + + bool prepare(const Selection &Inputs) override { + Selected = getSelectedRecord(Inputs, &FromBase); + if (!Selected) + return false; + if (!isClassOK(*Selected)) + return false; + if (FromBase) { + if (collectPureMethodsFromBase(*Selected, *FromBase, PureVirtualMethods)) + return false; + } else { + if (collectAllPureMethods(*Selected, PureVirtualMethods)) + return false; + } + return !PureVirtualMethods.empty(); + } + + Expected apply(const Selection &Inputs) override { + // We should have at least one pure virtual method to add. + assert(!PureVirtualMethods.empty() && + "Prepare returned true when no methodx existed"); + bool AccessNeedsProcessing[3] = {0}; + for (auto Item : PureVirtualMethods) { + AccessNeedsProcessing[Item.getInt()] = true; + } + + PrintingInContextCallback Callbacks(Selected); + PrintingPolicy Policy = Selected->getASTContext().getPrintingPolicy(); + Policy.SuppressScope = false; + Policy.Callbacks = &Callbacks; + + auto InsertionPoints = getInsertionPoints(*Selected, AccessNeedsProcessing, + Inputs.AST->getSourceManager(), + Inputs.AST->getLangOpts()); + SmallString<256> Buffer; + llvm::raw_svector_ostream OS(Buffer); + tooling::Replacements Replacements; + for (auto &Item : InsertionPoints) { + assert(Item.Loc.isValid()); + if (!AccessNeedsProcessing[Item.Access]) + continue; + AccessNeedsProcessing[Item.Access] = false; + printMethods(OS, PureVirtualMethods, Item.Access, Policy, + /*PrintAccessSpec=*/false, /*CreateBody=*/DefineMethods); + if (auto Err = Replacements.add(tooling::Replacement( + Inputs.AST->getSourceManager(), Item.Loc, 0, Buffer))) { + return std::move(Err); + } + Buffer.clear(); + } + + // Any access specifiers not convered can be added in one insertion. + for (AccessSpecifier Spec : {AS_public, AS_protected, AS_private}) { + if (!AccessNeedsProcessing[Spec]) + continue; + printMethods(OS, PureVirtualMethods, Spec, Policy, + /*PrintAccessSpec=*/true, /*CreateBody=*/DefineMethods); + } + if (!Buffer.empty()) { + if (auto Err = Replacements.add(tooling::Replacement( + Inputs.AST->getSourceManager(), + Selected->getBraceRange().getEnd(), 0, Buffer))) { + Err = llvm::handleErrors( + std::move(Err), + [&](const tooling::ReplacementError &RE) -> llvm::Error { + // If we have a conflict where 2 items want to insert into the + // same place, just append the second insert after the first. This + // case can occur when a class has an empty body, with no + // whitespace e.g. class A : public Base {}; and has methods to + // insert with different access specifiers. + // FIXME: It may make more sense to detect this conflict condition + // before we trying to add the replacements. + if (RE.get() != tooling::replacement_error::insert_conflict) + return llvm::make_error(RE); + const auto &Conflict = RE.getNewReplacement(); + tooling::Replacement NewR( + Conflict->getFilePath(), + Replacements.getShiftedCodePosition( + RE.getExistingReplacement()->getOffset()), + 0, Conflict->getReplacementText()); + Replacements = Replacements.merge(tooling::Replacements(NewR)); + return llvm::Error::success(); + }); + if (Err) + return std::move(Err); + } + } + return Effect::mainFileEdit(Inputs.AST->getASTContext().getSourceManager(), + std::move(Replacements)); + } + + std::string title() const override { + if (FromBase) { + assert(Selected); + PrintingInContextCallback Callbacks(Selected->getDeclContext()); + auto Policy = Selected->getParentASTContext().getPrintingPolicy(); + Policy.SuppressScope = false; + Policy.Callbacks = &Callbacks; + std::string Result = "Implement pure virtual methods from '"; + llvm::raw_string_ostream OS(Result); + FromBase->getTypeSourceInfo()->getType().print(OS, Policy); + OS << '\''; + OS.flush(); + return Result; + } + return "Implement pure virtual methods"; + } + + llvm::StringLiteral kind() const override { + return CodeAction::REFACTOR_KIND; + } + +private: + const CXXRecordDecl *Selected; + llvm::SmallVector PureVirtualMethods; + llvm::Optional FromBase; +}; + +REGISTER_TWEAK(ImplementAbstract) + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt --- a/clang-tools-extra/clangd/unittests/CMakeLists.txt +++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt @@ -118,6 +118,7 @@ tweaks/ExpandMacroTests.cpp tweaks/ExtractFunctionTests.cpp tweaks/ExtractVariableTests.cpp + tweaks/ImplementAbstractTests.cpp tweaks/ObjCLocalizeStringLiteralTests.cpp tweaks/PopulateSwitchTests.cpp tweaks/RawStringLiteralTests.cpp diff --git a/clang-tools-extra/clangd/unittests/tweaks/ImplementAbstractTests.cpp b/clang-tools-extra/clangd/unittests/tweaks/ImplementAbstractTests.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/unittests/tweaks/ImplementAbstractTests.cpp @@ -0,0 +1,415 @@ +//===-- ImplementAbstractTests.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 "TestTU.h" +#include "TweakTesting.h" +#include "gmock/gmock-matchers.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using ::testing::Not; + +namespace clang { +namespace clangd { +namespace { + +bool stringEqIgnoreWs(StringRef LHS, StringRef RHS) { + auto TrimmedL = LHS.trim(); + auto TrimmedR = RHS.trim(); + static constexpr llvm::StringLiteral WS(" \t\r\n\f\v"); + + while (!TrimmedL.empty() && !TrimmedR.empty()) { + auto LPos = TrimmedL.find_first_of(WS); + auto RPos = TrimmedR.find_first_of(WS); + if (TrimmedL.take_front(LPos) != TrimmedR.take_front(RPos)) + return false; + TrimmedL = + TrimmedL.substr(LPos).drop_while([](char C) { return WS.contains(C); }); + TrimmedR = + TrimmedR.substr(RPos).drop_while([](char C) { return WS.contains(C); }); + } + return TrimmedL == TrimmedR; +} + +MATCHER_P(STREQWS, EqualTo, "") { + if (stringEqIgnoreWs(arg, EqualTo)) + return true; + + auto Result = + testing::internal::EqFailure("", "", arg, std::string(EqualTo), false); + *result_listener << Result.message(); + return false; +} + +TWEAK_TEST(ImplementAbstract); + +TEST_F(ImplementAbstractTest, TestUnavailable) { + + StringRef Cases[]{ + // Not a pure virtual method. + R"cpp( + class A { + virtual void Foo(); + }; + class ^B : public A {}; + )cpp", + // Pure virtual method overridden in class. + R"cpp( + class A { + virtual void Foo() = 0; + }; + class ^B : public A { + void Foo() override; + }; + )cpp", + // Pure virtual method overridden in class with virtual keyword + R"cpp( + class A { + virtual void Foo() = 0; + }; + class ^B : public A { + virtual void Foo() override; + }; + )cpp", + // Pure virtual method overridden in class without override keyword + R"cpp( + class A { + virtual void Foo() = 0; + }; + class ^B : public A { + void Foo(); + }; + )cpp", + // Pure virtual method overriden in base class. + R"cpp( + class A { + virtual void Foo() = 0; + }; + class B : public A { + void Foo() override; + }; + class ^C : public B {}; + )cpp"}; + for (const auto &Case : Cases) { + EXPECT_THAT(Case, Not(isAvailable())); + } +} + +TEST_F(ImplementAbstractTest, NormalAvailable) { + struct Case { + llvm::StringRef TestHeader; + llvm::StringRef TestSource; + llvm::StringRef ExpectedSource; + }; + + Case Cases[]{ + { + R"cpp( + class A { + virtual void Foo() = 0; + };)cpp", + R"cpp( + class B : public A {^}; + )cpp", + R"cpp( + class B : public A { + void Foo() override { + } + }; + )cpp", + }, + { + R"cpp( + class A { + public: + virtual int Foo() = 0; + };)cpp", + R"cpp( + class ^B : public A {}; + )cpp", + R"cpp( + class B : public A { + public: + int Foo() override { + return {}; + } + }; + )cpp", + }, + { + R"cpp( + class A { + virtual void Foo(int Param) = 0; + };)cpp", + R"cpp( + class ^B : public A {}; + )cpp", + R"cpp( + class B : public A { + void Foo(int Param) override { + } + }; + )cpp", + }, + { + R"cpp( + class A { + virtual void Foo(int Param) = 0; + };)cpp", + R"cpp( + struct ^B : public A {}; + )cpp", + R"cpp( + struct B : public A { + private: + void Foo(int Param) override { + } + }; + )cpp", + }, + { + R"cpp( + class A { + virtual void Foo(int Param) const volatile = 0; + + public: + virtual void Bar(int Param) = 0; + };)cpp", + R"cpp( + class ^B : public A { + void Foo(int Param) const volatile override; + }; + )cpp", + R"cpp( + class B : public A { + void Foo(int Param) const volatile override; + + public: + void Bar(int Param) override { + } + }; + )cpp", + }, + { + R"cpp( + class A { + virtual void Foo() = 0; + virtual void Bar() = 0; + }; + class B : public A { + void Foo() override { + } + }; + )cpp", + R"cpp( + class ^C : public B { + virtual void Baz(); + }; + )cpp", + R"cpp( + class C : public B { + virtual void Baz(); + void Bar() override { + } + }; + )cpp", + }, + { + R"cpp( + class A { + virtual void Foo() = 0; + };)cpp", + R"cpp( + class ^B : public A { + ~B(); + }; + )cpp", + R"cpp( + class B : public A { + void Foo() override { + } + + ~B(); + }; + )cpp", + }, + { + R"cpp( + class A { + virtual void Foo() = 0; + + public: + virtual void Bar() = 0; + };)cpp", + R"cpp( + class ^B : public A {}; + )cpp", + R"cpp( + class B : public A { + void Foo() override { + } + + public: + void Bar() override { + } + }; + )cpp", + }, + { + R"cpp( + class A { + virtual void Foo() = 0; + }; + struct B : public A { + virtual void Bar() = 0; + };)cpp", + R"cpp( + class ^C : public B {}; + )cpp", + R"cpp( + class C : public B { + void Foo() override { + } + + public: + void Bar() override { + } + }; + )cpp", + }, + { + R"cpp( + class A { + virtual void Foo() = 0; + }; + struct B : public A { + virtual void Bar() = 0; + };)cpp", + R"cpp( + class ^C : private B {}; + )cpp", + R"cpp( + class C : private B { + void Foo() override { + } + void Bar() override { + } + }; + )cpp", + }, + { + R"cpp( + struct A { + virtual void Foo() = 0; + }; + struct B { + virtual void Bar() = 0; + };)cpp", + R"cpp( + class C : public ^A, B {}; + )cpp", + R"cpp( + class C : public A, B { + + public: + void Foo() override { + } + }; + )cpp", + }, + { + R"cpp( + struct A { + virtual void Foo() = 0; + }; + struct B { + virtual void Bar() = 0; + };)cpp", + R"cpp( + class ^C : public A, B {}; + )cpp", + R"cpp( + class C : public A, B { + void Bar() override { + } + + public: + void Foo() override { + } + }; + )cpp", + }, + }; + + for (const auto &Case : Cases) { + Header = Case.TestHeader.str(); + EXPECT_THAT(apply(Case.TestSource), STREQWS(Case.ExpectedSource)); + } +} + +TEST_F(ImplementAbstractTest, TemplateUnavailable) { + StringRef Cases[]{ + R"cpp( + template class A { virtual void Foo() = 0; }; + template class ^B : public A {}; + )cpp", + R"cpp( + template class ^B : public T{}; + )cpp", + }; + for (const auto &Case : Cases) { + EXPECT_THAT(Case, Not(isAvailable())); + } +} + +TEST_F(ImplementAbstractTest, TemplateAvailable) { + struct Case { + llvm::StringRef TestHeader; + llvm::StringRef TestSource; + llvm::StringRef ExpectedSource; + }; + Case Cases[]{ + { + R"cpp( + template class A { virtual void Foo() = 0; }; + )cpp", + R"cpp( + class ^B : public A {}; + )cpp", + R"cpp( + class B : public A { + void Foo() override { + } + }; + )cpp", + }, + { + R"cpp( + class A { + virtual void Foo() = 0; + };)cpp", + R"cpp( + template class ^B : public A {}; + )cpp", + R"cpp( + template class B : public A { + void Foo() override { + } + }; + )cpp", + }, + }; + for (const auto &Case : Cases) { + Header = Case.TestHeader.str(); + EXPECT_THAT(apply(Case.TestSource), STREQWS(Case.ExpectedSource)); + } +} + +} // namespace +} // namespace clangd +} // namespace clang