Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 1 | //===--- FindSymbols.cpp ------------------------------------*- C++-*------===// |
| 2 | // |
| 3 | // The LLVM Compiler Infrastructure |
| 4 | // |
| 5 | // This file is distributed under the University of Illinois Open Source |
| 6 | // License. See LICENSE.TXT for details. |
| 7 | // |
| 8 | //===----------------------------------------------------------------------===// |
| 9 | #include "FindSymbols.h" |
| 10 | |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 11 | #include "AST.h" |
| 12 | #include "ClangdUnit.h" |
Sam McCall | 203cdee | 2018-06-07 06:55:59 +0000 | [diff] [blame] | 13 | #include "FuzzyMatch.h" |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 14 | #include "Logger.h" |
Sam McCall | 203cdee | 2018-06-07 06:55:59 +0000 | [diff] [blame] | 15 | #include "Quality.h" |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 16 | #include "SourceCode.h" |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 17 | #include "index/Index.h" |
Ilya Biryukov | 19d7560 | 2018-11-23 15:21:19 +0000 | [diff] [blame] | 18 | #include "clang/AST/DeclTemplate.h" |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 19 | #include "clang/Index/IndexDataConsumer.h" |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 20 | #include "clang/Index/IndexSymbol.h" |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 21 | #include "clang/Index/IndexingAction.h" |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 22 | #include "llvm/Support/FormatVariadic.h" |
| 23 | #include "llvm/Support/Path.h" |
Ilya Biryukov | 19d7560 | 2018-11-23 15:21:19 +0000 | [diff] [blame] | 24 | #include "llvm/Support/ScopedPrinter.h" |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 25 | |
Sam McCall | 203cdee | 2018-06-07 06:55:59 +0000 | [diff] [blame] | 26 | #define DEBUG_TYPE "FindSymbols" |
| 27 | |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 28 | namespace clang { |
| 29 | namespace clangd { |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 30 | namespace { |
| 31 | |
| 32 | // Convert a index::SymbolKind to clangd::SymbolKind (LSP) |
| 33 | // Note, some are not perfect matches and should be improved when this LSP |
| 34 | // issue is addressed: |
| 35 | // https://github.com/Microsoft/language-server-protocol/issues/344 |
| 36 | SymbolKind indexSymbolKindToSymbolKind(index::SymbolKind Kind) { |
| 37 | switch (Kind) { |
| 38 | case index::SymbolKind::Unknown: |
| 39 | return SymbolKind::Variable; |
| 40 | case index::SymbolKind::Module: |
| 41 | return SymbolKind::Module; |
| 42 | case index::SymbolKind::Namespace: |
| 43 | return SymbolKind::Namespace; |
| 44 | case index::SymbolKind::NamespaceAlias: |
| 45 | return SymbolKind::Namespace; |
| 46 | case index::SymbolKind::Macro: |
| 47 | return SymbolKind::String; |
| 48 | case index::SymbolKind::Enum: |
| 49 | return SymbolKind::Enum; |
| 50 | case index::SymbolKind::Struct: |
| 51 | return SymbolKind::Struct; |
| 52 | case index::SymbolKind::Class: |
| 53 | return SymbolKind::Class; |
| 54 | case index::SymbolKind::Protocol: |
| 55 | return SymbolKind::Interface; |
| 56 | case index::SymbolKind::Extension: |
| 57 | return SymbolKind::Interface; |
| 58 | case index::SymbolKind::Union: |
| 59 | return SymbolKind::Class; |
| 60 | case index::SymbolKind::TypeAlias: |
| 61 | return SymbolKind::Class; |
| 62 | case index::SymbolKind::Function: |
| 63 | return SymbolKind::Function; |
| 64 | case index::SymbolKind::Variable: |
| 65 | return SymbolKind::Variable; |
| 66 | case index::SymbolKind::Field: |
| 67 | return SymbolKind::Field; |
| 68 | case index::SymbolKind::EnumConstant: |
| 69 | return SymbolKind::EnumMember; |
| 70 | case index::SymbolKind::InstanceMethod: |
| 71 | case index::SymbolKind::ClassMethod: |
| 72 | case index::SymbolKind::StaticMethod: |
| 73 | return SymbolKind::Method; |
| 74 | case index::SymbolKind::InstanceProperty: |
| 75 | case index::SymbolKind::ClassProperty: |
| 76 | case index::SymbolKind::StaticProperty: |
| 77 | return SymbolKind::Property; |
| 78 | case index::SymbolKind::Constructor: |
| 79 | case index::SymbolKind::Destructor: |
| 80 | return SymbolKind::Method; |
| 81 | case index::SymbolKind::ConversionFunction: |
| 82 | return SymbolKind::Function; |
| 83 | case index::SymbolKind::Parameter: |
| 84 | return SymbolKind::Variable; |
| 85 | case index::SymbolKind::Using: |
| 86 | return SymbolKind::Namespace; |
| 87 | } |
| 88 | llvm_unreachable("invalid symbol kind"); |
| 89 | } |
| 90 | |
Sam McCall | 203cdee | 2018-06-07 06:55:59 +0000 | [diff] [blame] | 91 | using ScoredSymbolInfo = std::pair<float, SymbolInformation>; |
| 92 | struct ScoredSymbolGreater { |
| 93 | bool operator()(const ScoredSymbolInfo &L, const ScoredSymbolInfo &R) { |
| 94 | if (L.first != R.first) |
| 95 | return L.first > R.first; |
| 96 | return L.second.name < R.second.name; // Earlier name is better. |
| 97 | } |
| 98 | }; |
| 99 | |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 100 | } // namespace |
| 101 | |
Ilya Biryukov | f2001aa | 2019-01-07 15:45:19 +0000 | [diff] [blame] | 102 | llvm::Expected<std::vector<SymbolInformation>> |
| 103 | getWorkspaceSymbols(llvm::StringRef Query, int Limit, |
| 104 | const SymbolIndex *const Index, llvm::StringRef HintPath) { |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 105 | std::vector<SymbolInformation> Result; |
| 106 | if (Query.empty() || !Index) |
| 107 | return Result; |
| 108 | |
| 109 | auto Names = splitQualifiedName(Query); |
| 110 | |
| 111 | FuzzyFindRequest Req; |
| 112 | Req.Query = Names.second; |
| 113 | |
| 114 | // FuzzyFind doesn't want leading :: qualifier |
| 115 | bool IsGlobalQuery = Names.first.consume_front("::"); |
| 116 | // Restrict results to the scope in the query string if present (global or |
| 117 | // not). |
| 118 | if (IsGlobalQuery || !Names.first.empty()) |
| 119 | Req.Scopes = {Names.first}; |
Eric Liu | b04869a | 2018-11-06 11:08:17 +0000 | [diff] [blame] | 120 | else |
| 121 | Req.AnyScope = true; |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 122 | if (Limit) |
Kirill Bobyrev | e6dd080 | 2018-09-13 14:27:03 +0000 | [diff] [blame] | 123 | Req.Limit = Limit; |
| 124 | TopN<ScoredSymbolInfo, ScoredSymbolGreater> Top( |
| 125 | Req.Limit ? *Req.Limit : std::numeric_limits<size_t>::max()); |
Sam McCall | 203cdee | 2018-06-07 06:55:59 +0000 | [diff] [blame] | 126 | FuzzyMatcher Filter(Req.Query); |
Eric Liu | 53425f29 | 2018-06-19 09:33:53 +0000 | [diff] [blame] | 127 | Index->fuzzyFind(Req, [HintPath, &Top, &Filter](const Symbol &Sym) { |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 128 | // Prefer the definition over e.g. a function declaration in a header |
| 129 | auto &CD = Sym.Definition ? Sym.Definition : Sym.CanonicalDeclaration; |
| 130 | auto Uri = URI::parse(CD.FileURI); |
| 131 | if (!Uri) { |
Sam McCall | bed5885 | 2018-07-11 10:35:11 +0000 | [diff] [blame] | 132 | log("Workspace symbol: Could not parse URI '{0}' for symbol '{1}'.", |
| 133 | CD.FileURI, Sym.Name); |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 134 | return; |
| 135 | } |
Eric Liu | 53425f29 | 2018-06-19 09:33:53 +0000 | [diff] [blame] | 136 | auto Path = URI::resolve(*Uri, HintPath); |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 137 | if (!Path) { |
Sam McCall | bed5885 | 2018-07-11 10:35:11 +0000 | [diff] [blame] | 138 | log("Workspace symbol: Could not resolve path for URI '{0}' for symbol " |
| 139 | "'{1}'.", |
| 140 | Uri->toString(), Sym.Name); |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 141 | return; |
| 142 | } |
| 143 | Location L; |
Eric Liu | 4d814a9 | 2018-11-28 10:30:42 +0000 | [diff] [blame] | 144 | // Use HintPath as TUPath since there is no TU associated with this |
| 145 | // request. |
| 146 | L.uri = URIForFile::canonicalize(*Path, HintPath); |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 147 | Position Start, End; |
Haojian Wu | b515fab | 2018-10-18 10:43:50 +0000 | [diff] [blame] | 148 | Start.line = CD.Start.line(); |
| 149 | Start.character = CD.Start.column(); |
| 150 | End.line = CD.End.line(); |
| 151 | End.character = CD.End.column(); |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 152 | L.range = {Start, End}; |
| 153 | SymbolKind SK = indexSymbolKindToSymbolKind(Sym.SymInfo.Kind); |
| 154 | std::string Scope = Sym.Scope; |
Ilya Biryukov | f2001aa | 2019-01-07 15:45:19 +0000 | [diff] [blame] | 155 | llvm::StringRef ScopeRef = Scope; |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 156 | ScopeRef.consume_back("::"); |
Sam McCall | 203cdee | 2018-06-07 06:55:59 +0000 | [diff] [blame] | 157 | SymbolInformation Info = {Sym.Name, SK, L, ScopeRef}; |
| 158 | |
| 159 | SymbolQualitySignals Quality; |
| 160 | Quality.merge(Sym); |
| 161 | SymbolRelevanceSignals Relevance; |
| 162 | Relevance.Query = SymbolRelevanceSignals::Generic; |
| 163 | if (auto NameMatch = Filter.match(Sym.Name)) |
| 164 | Relevance.NameMatch = *NameMatch; |
| 165 | else { |
Sam McCall | bed5885 | 2018-07-11 10:35:11 +0000 | [diff] [blame] | 166 | log("Workspace symbol: {0} didn't match query {1}", Sym.Name, |
| 167 | Filter.pattern()); |
Sam McCall | 203cdee | 2018-06-07 06:55:59 +0000 | [diff] [blame] | 168 | return; |
| 169 | } |
| 170 | Relevance.merge(Sym); |
| 171 | auto Score = |
| 172 | evaluateSymbolAndRelevance(Quality.evaluate(), Relevance.evaluate()); |
Sam McCall | bed5885 | 2018-07-11 10:35:11 +0000 | [diff] [blame] | 173 | dlog("FindSymbols: {0}{1} = {2}\n{3}{4}\n", Sym.Scope, Sym.Name, Score, |
| 174 | Quality, Relevance); |
Sam McCall | 203cdee | 2018-06-07 06:55:59 +0000 | [diff] [blame] | 175 | |
| 176 | Top.push({Score, std::move(Info)}); |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 177 | }); |
Sam McCall | 203cdee | 2018-06-07 06:55:59 +0000 | [diff] [blame] | 178 | for (auto &R : std::move(Top).items()) |
| 179 | Result.push_back(std::move(R.second)); |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 180 | return Result; |
| 181 | } |
| 182 | |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 183 | namespace { |
Ilya Biryukov | 19d7560 | 2018-11-23 15:21:19 +0000 | [diff] [blame] | 184 | llvm::Optional<DocumentSymbol> declToSym(ASTContext &Ctx, const NamedDecl &ND) { |
| 185 | auto &SM = Ctx.getSourceManager(); |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 186 | |
Ilya Biryukov | 19d7560 | 2018-11-23 15:21:19 +0000 | [diff] [blame] | 187 | SourceLocation NameLoc = findNameLoc(&ND); |
| 188 | // getFileLoc is a good choice for us, but we also need to make sure |
| 189 | // sourceLocToPosition won't switch files, so we call getSpellingLoc on top of |
| 190 | // that to make sure it does not switch files. |
| 191 | // FIXME: sourceLocToPosition should not switch files! |
Ilya Biryukov | 22fa465 | 2019-01-03 13:28:05 +0000 | [diff] [blame] | 192 | SourceLocation BeginLoc = SM.getSpellingLoc(SM.getFileLoc(ND.getBeginLoc())); |
Ilya Biryukov | 19d7560 | 2018-11-23 15:21:19 +0000 | [diff] [blame] | 193 | SourceLocation EndLoc = SM.getSpellingLoc(SM.getFileLoc(ND.getEndLoc())); |
| 194 | if (NameLoc.isInvalid() || BeginLoc.isInvalid() || EndLoc.isInvalid()) |
| 195 | return llvm::None; |
| 196 | |
| 197 | if (!SM.isWrittenInMainFile(NameLoc) || !SM.isWrittenInMainFile(BeginLoc) || |
| 198 | !SM.isWrittenInMainFile(EndLoc)) |
| 199 | return llvm::None; |
| 200 | |
| 201 | Position NameBegin = sourceLocToPosition(SM, NameLoc); |
| 202 | Position NameEnd = sourceLocToPosition( |
| 203 | SM, Lexer::getLocForEndOfToken(NameLoc, 0, SM, Ctx.getLangOpts())); |
| 204 | |
| 205 | index::SymbolInfo SymInfo = index::getSymbolInfo(&ND); |
| 206 | // FIXME: this is not classifying constructors, destructors and operators |
| 207 | // correctly (they're all "methods"). |
| 208 | SymbolKind SK = indexSymbolKindToSymbolKind(SymInfo.Kind); |
| 209 | |
| 210 | DocumentSymbol SI; |
| 211 | SI.name = printName(Ctx, ND); |
| 212 | SI.kind = SK; |
| 213 | SI.deprecated = ND.isDeprecated(); |
| 214 | SI.range = |
| 215 | Range{sourceLocToPosition(SM, BeginLoc), sourceLocToPosition(SM, EndLoc)}; |
| 216 | SI.selectionRange = Range{NameBegin, NameEnd}; |
| 217 | if (!SI.range.contains(SI.selectionRange)) { |
| 218 | // 'selectionRange' must be contained in 'range', so in cases where clang |
| 219 | // reports unrelated ranges we need to reconcile somehow. |
| 220 | SI.range = SI.selectionRange; |
| 221 | } |
| 222 | return SI; |
| 223 | } |
| 224 | |
| 225 | /// A helper class to build an outline for the parse AST. It traverse the AST |
| 226 | /// directly instead of using RecursiveASTVisitor (RAV) for three main reasons: |
| 227 | /// - there is no way to keep RAV from traversing subtrees we're not |
| 228 | /// interested in. E.g. not traversing function locals or implicit template |
| 229 | /// instantiations. |
| 230 | /// - it's easier to combine results of recursive passes, e.g. |
| 231 | /// - visiting decls is actually simple, so we don't hit the complicated |
| 232 | /// cases that RAV mostly helps with (types and expressions, etc.) |
| 233 | class DocumentOutline { |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 234 | public: |
Ilya Biryukov | 19d7560 | 2018-11-23 15:21:19 +0000 | [diff] [blame] | 235 | DocumentOutline(ParsedAST &AST) : AST(AST) {} |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 236 | |
Ilya Biryukov | 19d7560 | 2018-11-23 15:21:19 +0000 | [diff] [blame] | 237 | /// Builds the document outline for the generated AST. |
| 238 | std::vector<DocumentSymbol> build() { |
| 239 | std::vector<DocumentSymbol> Results; |
| 240 | for (auto &TopLevel : AST.getLocalTopLevelDecls()) |
| 241 | traverseDecl(TopLevel, Results); |
| 242 | return Results; |
| 243 | } |
| 244 | |
| 245 | private: |
| 246 | enum class VisitKind { No, OnlyDecl, DeclAndChildren }; |
| 247 | |
| 248 | void traverseDecl(Decl *D, std::vector<DocumentSymbol> &Results) { |
| 249 | if (auto *Templ = llvm::dyn_cast<TemplateDecl>(D)) |
| 250 | D = Templ->getTemplatedDecl(); |
| 251 | auto *ND = llvm::dyn_cast<NamedDecl>(D); |
| 252 | if (!ND) |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 253 | return; |
Ilya Biryukov | 19d7560 | 2018-11-23 15:21:19 +0000 | [diff] [blame] | 254 | VisitKind Visit = shouldVisit(ND); |
| 255 | if (Visit == VisitKind::No) |
| 256 | return; |
| 257 | llvm::Optional<DocumentSymbol> Sym = declToSym(AST.getASTContext(), *ND); |
| 258 | if (!Sym) |
| 259 | return; |
| 260 | if (Visit == VisitKind::DeclAndChildren) |
| 261 | traverseChildren(D, Sym->children); |
| 262 | Results.push_back(std::move(*Sym)); |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 263 | } |
| 264 | |
Ilya Biryukov | 19d7560 | 2018-11-23 15:21:19 +0000 | [diff] [blame] | 265 | void traverseChildren(Decl *D, std::vector<DocumentSymbol> &Results) { |
| 266 | auto *Scope = llvm::dyn_cast<DeclContext>(D); |
| 267 | if (!Scope) |
| 268 | return; |
| 269 | for (auto *C : Scope->decls()) |
| 270 | traverseDecl(C, Results); |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 271 | } |
| 272 | |
Ilya Biryukov | 19d7560 | 2018-11-23 15:21:19 +0000 | [diff] [blame] | 273 | VisitKind shouldVisit(NamedDecl *D) { |
| 274 | if (D->isImplicit()) |
| 275 | return VisitKind::No; |
| 276 | |
| 277 | if (auto Func = llvm::dyn_cast<FunctionDecl>(D)) { |
| 278 | // Some functions are implicit template instantiations, those should be |
| 279 | // ignored. |
| 280 | if (auto *Info = Func->getTemplateSpecializationInfo()) { |
| 281 | if (!Info->isExplicitInstantiationOrSpecialization()) |
| 282 | return VisitKind::No; |
| 283 | } |
| 284 | // Only visit the function itself, do not visit the children (i.e. |
| 285 | // function parameters, etc.) |
| 286 | return VisitKind::OnlyDecl; |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 287 | } |
Ilya Biryukov | 19d7560 | 2018-11-23 15:21:19 +0000 | [diff] [blame] | 288 | // Handle template instantiations. We have three cases to consider: |
| 289 | // - explicit instantiations, e.g. 'template class std::vector<int>;' |
| 290 | // Visit the decl itself (it's present in the code), but not the |
| 291 | // children. |
| 292 | // - implicit instantiations, i.e. not written by the user. |
| 293 | // Do not visit at all, they are not present in the code. |
| 294 | // - explicit specialization, e.g. 'template <> class vector<bool> {};' |
| 295 | // Visit both the decl and its children, both are written in the code. |
| 296 | if (auto *TemplSpec = llvm::dyn_cast<ClassTemplateSpecializationDecl>(D)) { |
| 297 | if (TemplSpec->isExplicitInstantiationOrSpecialization()) |
| 298 | return TemplSpec->isExplicitSpecialization() |
| 299 | ? VisitKind::DeclAndChildren |
| 300 | : VisitKind::OnlyDecl; |
| 301 | return VisitKind::No; |
| 302 | } |
| 303 | if (auto *TemplSpec = llvm::dyn_cast<VarTemplateSpecializationDecl>(D)) { |
| 304 | if (TemplSpec->isExplicitInstantiationOrSpecialization()) |
| 305 | return TemplSpec->isExplicitSpecialization() |
| 306 | ? VisitKind::DeclAndChildren |
| 307 | : VisitKind::OnlyDecl; |
| 308 | return VisitKind::No; |
| 309 | } |
| 310 | // For all other cases, visit both the children and the decl. |
| 311 | return VisitKind::DeclAndChildren; |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 312 | } |
Ilya Biryukov | 19d7560 | 2018-11-23 15:21:19 +0000 | [diff] [blame] | 313 | |
| 314 | ParsedAST &AST; |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 315 | }; |
Ilya Biryukov | 19d7560 | 2018-11-23 15:21:19 +0000 | [diff] [blame] | 316 | |
| 317 | std::vector<DocumentSymbol> collectDocSymbols(ParsedAST &AST) { |
| 318 | return DocumentOutline(AST).build(); |
| 319 | } |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 320 | } // namespace |
| 321 | |
Ilya Biryukov | 19d7560 | 2018-11-23 15:21:19 +0000 | [diff] [blame] | 322 | llvm::Expected<std::vector<DocumentSymbol>> getDocumentSymbols(ParsedAST &AST) { |
| 323 | return collectDocSymbols(AST); |
Marc-Andre Laperle | 1be6970 | 2018-07-05 19:35:01 +0000 | [diff] [blame] | 324 | } |
| 325 | |
Marc-Andre Laperle | b387b6e | 2018-04-23 20:00:52 +0000 | [diff] [blame] | 326 | } // namespace clangd |
| 327 | } // namespace clang |