clang-tools 20.0.0git
DefineOutline.cpp
Go to the documentation of this file.
1//===--- DefineOutline.cpp ---------------------------------------*- C++-*-===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
9#include "AST.h"
10#include "FindTarget.h"
11#include "HeaderSourceSwitch.h"
12#include "ParsedAST.h"
13#include "Selection.h"
14#include "SourceCode.h"
15#include "refactor/Tweak.h"
16#include "support/Logger.h"
17#include "support/Path.h"
18#include "clang/AST/ASTTypeTraits.h"
19#include "clang/AST/Attr.h"
20#include "clang/AST/Decl.h"
21#include "clang/AST/DeclBase.h"
22#include "clang/AST/DeclCXX.h"
23#include "clang/AST/DeclTemplate.h"
24#include "clang/AST/Stmt.h"
25#include "clang/Basic/SourceLocation.h"
26#include "clang/Basic/SourceManager.h"
27#include "clang/Basic/TokenKinds.h"
28#include "clang/Tooling/Core/Replacement.h"
29#include "clang/Tooling/Syntax/Tokens.h"
30#include "llvm/ADT/STLExtras.h"
31#include "llvm/ADT/StringRef.h"
32#include "llvm/Support/Casting.h"
33#include "llvm/Support/Error.h"
34#include <cstddef>
35#include <optional>
36#include <string>
37
38namespace clang {
39namespace clangd {
40namespace {
41
42// Deduces the FunctionDecl from a selection. Requires either the function body
43// or the function decl to be selected. Returns null if none of the above
44// criteria is met.
45// FIXME: This is shared with define inline, move them to a common header once
46// we have a place for such.
47const FunctionDecl *getSelectedFunction(const SelectionTree::Node *SelNode) {
48 if (!SelNode)
49 return nullptr;
50 const DynTypedNode &AstNode = SelNode->ASTNode;
51 if (const FunctionDecl *FD = AstNode.get<FunctionDecl>())
52 return FD;
53 if (AstNode.get<CompoundStmt>() &&
54 SelNode->Selected == SelectionTree::Complete) {
55 if (const SelectionTree::Node *P = SelNode->Parent)
56 return P->ASTNode.get<FunctionDecl>();
57 }
58 return nullptr;
59}
60
61std::optional<Path> getSourceFile(llvm::StringRef FileName,
62 const Tweak::Selection &Sel) {
63 assert(Sel.FS);
64 if (auto Source = getCorrespondingHeaderOrSource(FileName, Sel.FS))
65 return *Source;
66 return getCorrespondingHeaderOrSource(FileName, *Sel.AST, Sel.Index);
67}
68
69// Synthesize a DeclContext for TargetNS from CurContext. TargetNS must be empty
70// for global namespace, and endwith "::" otherwise.
71// Returns std::nullopt if TargetNS is not a prefix of CurContext.
72std::optional<const DeclContext *>
73findContextForNS(llvm::StringRef TargetNS, const DeclContext *CurContext) {
74 assert(TargetNS.empty() || TargetNS.ends_with("::"));
75 // Skip any non-namespace contexts, e.g. TagDecls, functions/methods.
76 CurContext = CurContext->getEnclosingNamespaceContext();
77 // If TargetNS is empty, it means global ns, which is translation unit.
78 if (TargetNS.empty()) {
79 while (!CurContext->isTranslationUnit())
80 CurContext = CurContext->getParent();
81 return CurContext;
82 }
83 // Otherwise we need to drop any trailing namespaces from CurContext until
84 // we reach TargetNS.
85 std::string TargetContextNS =
86 CurContext->isNamespace()
87 ? llvm::cast<NamespaceDecl>(CurContext)->getQualifiedNameAsString()
88 : "";
89 TargetContextNS.append("::");
90
91 llvm::StringRef CurrentContextNS(TargetContextNS);
92 // If TargetNS is not a prefix of CurrentContext, there's no way to reach
93 // it.
94 if (!CurrentContextNS.starts_with(TargetNS))
95 return std::nullopt;
96
97 while (CurrentContextNS != TargetNS) {
98 CurContext = CurContext->getParent();
99 // These colons always exists since TargetNS is a prefix of
100 // CurrentContextNS, it ends with "::" and they are not equal.
101 CurrentContextNS = CurrentContextNS.take_front(
102 CurrentContextNS.drop_back(2).rfind("::") + 2);
103 }
104 return CurContext;
105}
106
107// Returns source code for FD after applying Replacements.
108// FIXME: Make the function take a parameter to return only the function body,
109// afterwards it can be shared with define-inline code action.
110llvm::Expected<std::string>
111getFunctionSourceAfterReplacements(const FunctionDecl *FD,
112 const tooling::Replacements &Replacements) {
113 const auto &SM = FD->getASTContext().getSourceManager();
114 auto OrigFuncRange = toHalfOpenFileRange(
115 SM, FD->getASTContext().getLangOpts(), FD->getSourceRange());
116 if (!OrigFuncRange)
117 return error("Couldn't get range for function.");
118 assert(!FD->getDescribedFunctionTemplate() &&
119 "Define out-of-line doesn't apply to function templates.");
120
121 // Get new begin and end positions for the qualified function definition.
122 unsigned FuncBegin = SM.getFileOffset(OrigFuncRange->getBegin());
123 unsigned FuncEnd = Replacements.getShiftedCodePosition(
124 SM.getFileOffset(OrigFuncRange->getEnd()));
125
126 // Trim the result to function definition.
127 auto QualifiedFunc = tooling::applyAllReplacements(
128 SM.getBufferData(SM.getMainFileID()), Replacements);
129 if (!QualifiedFunc)
130 return QualifiedFunc.takeError();
131 return QualifiedFunc->substr(FuncBegin, FuncEnd - FuncBegin + 1);
132}
133
134// Returns replacements to delete tokens with kind `Kind` in the range
135// `FromRange`. Removes matching instances of given token preceeding the
136// function defition.
137llvm::Expected<tooling::Replacements>
138deleteTokensWithKind(const syntax::TokenBuffer &TokBuf, tok::TokenKind Kind,
139 SourceRange FromRange) {
140 tooling::Replacements DelKeywordCleanups;
141 llvm::Error Errors = llvm::Error::success();
142 bool FoundAny = false;
143 for (const auto &Tok : TokBuf.expandedTokens(FromRange)) {
144 if (Tok.kind() != Kind)
145 continue;
146 FoundAny = true;
147 auto Spelling = TokBuf.spelledForExpanded(llvm::ArrayRef(Tok));
148 if (!Spelling) {
149 Errors = llvm::joinErrors(
150 std::move(Errors),
151 error("define outline: couldn't remove `{0}` keyword.",
152 tok::getKeywordSpelling(Kind)));
153 break;
154 }
155 auto &SM = TokBuf.sourceManager();
156 CharSourceRange DelRange =
157 syntax::Token::range(SM, Spelling->front(), Spelling->back())
158 .toCharRange(SM);
159 if (auto Err =
160 DelKeywordCleanups.add(tooling::Replacement(SM, DelRange, "")))
161 Errors = llvm::joinErrors(std::move(Errors), std::move(Err));
162 }
163 if (!FoundAny) {
164 Errors = llvm::joinErrors(
165 std::move(Errors),
166 error("define outline: couldn't find `{0}` keyword to remove.",
167 tok::getKeywordSpelling(Kind)));
168 }
169
170 if (Errors)
171 return std::move(Errors);
172 return DelKeywordCleanups;
173}
174
175// Creates a modified version of function definition that can be inserted at a
176// different location, qualifies return value and function name to achieve that.
177// Contains function signature, except defaulted parameter arguments, body and
178// template parameters if applicable. No need to qualify parameters, as they are
179// looked up in the context containing the function/method.
180// FIXME: Drop attributes in function signature.
181llvm::Expected<std::string>
182getFunctionSourceCode(const FunctionDecl *FD, const DeclContext *TargetContext,
183 const syntax::TokenBuffer &TokBuf,
184 const HeuristicResolver *Resolver) {
185 auto &AST = FD->getASTContext();
186 auto &SM = AST.getSourceManager();
187
188 llvm::Error Errors = llvm::Error::success();
189 tooling::Replacements DeclarationCleanups;
190
191 // Finds the first unqualified name in function return type and name, then
192 // qualifies those to be valid in TargetContext.
194 FD,
195 [&](ReferenceLoc Ref) {
196 // It is enough to qualify the first qualifier, so skip references with
197 // a qualifier. Also we can't do much if there are no targets or name is
198 // inside a macro body.
199 if (Ref.Qualifier || Ref.Targets.empty() || Ref.NameLoc.isMacroID())
200 return;
201 // Only qualify return type and function name.
202 if (Ref.NameLoc != FD->getReturnTypeSourceRange().getBegin() &&
203 Ref.NameLoc != FD->getLocation())
204 return;
205
206 for (const NamedDecl *ND : Ref.Targets) {
207 if (ND->getDeclContext() != Ref.Targets.front()->getDeclContext()) {
208 elog("Targets from multiple contexts: {0}, {1}",
209 printQualifiedName(*Ref.Targets.front()),
210 printQualifiedName(*ND));
211 return;
212 }
213 }
214 const NamedDecl *ND = Ref.Targets.front();
215 const std::string Qualifier =
216 getQualification(AST, TargetContext,
217 SM.getLocForStartOfFile(SM.getMainFileID()), ND);
218 if (auto Err = DeclarationCleanups.add(
219 tooling::Replacement(SM, Ref.NameLoc, 0, Qualifier)))
220 Errors = llvm::joinErrors(std::move(Errors), std::move(Err));
221 },
222 Resolver);
223
224 // findExplicitReferences doesn't provide references to
225 // constructor/destructors, it only provides references to type names inside
226 // them.
227 // this works for constructors, but doesn't work for destructor as type name
228 // doesn't cover leading `~`, so handle it specially.
229 if (const auto *Destructor = llvm::dyn_cast<CXXDestructorDecl>(FD)) {
230 if (auto Err = DeclarationCleanups.add(tooling::Replacement(
231 SM, Destructor->getLocation(), 0,
232 getQualification(AST, TargetContext,
233 SM.getLocForStartOfFile(SM.getMainFileID()),
234 Destructor))))
235 Errors = llvm::joinErrors(std::move(Errors), std::move(Err));
236 }
237
238 // Get rid of default arguments, since they should not be specified in
239 // out-of-line definition.
240 for (const auto *PVD : FD->parameters()) {
241 if (!PVD->hasDefaultArg())
242 continue;
243 // Deletion range spans the initializer, usually excluding the `=`.
244 auto DelRange = CharSourceRange::getTokenRange(PVD->getDefaultArgRange());
245 // Get all tokens before the default argument.
246 auto Tokens = TokBuf.expandedTokens(PVD->getSourceRange())
247 .take_while([&SM, &DelRange](const syntax::Token &Tok) {
248 return SM.isBeforeInTranslationUnit(
249 Tok.location(), DelRange.getBegin());
250 });
251 if (TokBuf.expandedTokens(DelRange.getAsRange()).front().kind() !=
252 tok::equal) {
253 // Find the last `=` if it isn't included in the initializer, and update
254 // the DelRange to include it.
255 auto Tok =
256 llvm::find_if(llvm::reverse(Tokens), [](const syntax::Token &Tok) {
257 return Tok.kind() == tok::equal;
258 });
259 assert(Tok != Tokens.rend());
260 DelRange.setBegin(Tok->location());
261 }
262 if (auto Err =
263 DeclarationCleanups.add(tooling::Replacement(SM, DelRange, "")))
264 Errors = llvm::joinErrors(std::move(Errors), std::move(Err));
265 }
266
267 auto DelAttr = [&](const Attr *A) {
268 if (!A)
269 return;
270 auto AttrTokens =
271 TokBuf.spelledForExpanded(TokBuf.expandedTokens(A->getRange()));
272 assert(A->getLocation().isValid());
273 if (!AttrTokens || AttrTokens->empty()) {
274 Errors = llvm::joinErrors(
275 std::move(Errors), error("define outline: Can't move out of line as "
276 "function has a macro `{0}` specifier.",
277 A->getSpelling()));
278 return;
279 }
280 CharSourceRange DelRange =
281 syntax::Token::range(SM, AttrTokens->front(), AttrTokens->back())
282 .toCharRange(SM);
283 if (auto Err =
284 DeclarationCleanups.add(tooling::Replacement(SM, DelRange, "")))
285 Errors = llvm::joinErrors(std::move(Errors), std::move(Err));
286 };
287
288 DelAttr(FD->getAttr<OverrideAttr>());
289 DelAttr(FD->getAttr<FinalAttr>());
290
291 auto DelKeyword = [&](tok::TokenKind Kind, SourceRange FromRange) {
292 auto DelKeywords = deleteTokensWithKind(TokBuf, Kind, FromRange);
293 if (!DelKeywords) {
294 Errors = llvm::joinErrors(std::move(Errors), DelKeywords.takeError());
295 return;
296 }
297 DeclarationCleanups = DeclarationCleanups.merge(*DelKeywords);
298 };
299
300 if (FD->isInlineSpecified())
301 DelKeyword(tok::kw_inline, {FD->getBeginLoc(), FD->getLocation()});
302 if (const auto *MD = dyn_cast<CXXMethodDecl>(FD)) {
303 if (MD->isVirtualAsWritten())
304 DelKeyword(tok::kw_virtual, {FD->getBeginLoc(), FD->getLocation()});
305 if (MD->isStatic())
306 DelKeyword(tok::kw_static, {FD->getBeginLoc(), FD->getLocation()});
307 }
308 if (const auto *CD = dyn_cast<CXXConstructorDecl>(FD)) {
309 if (CD->isExplicit())
310 DelKeyword(tok::kw_explicit, {FD->getBeginLoc(), FD->getLocation()});
311 }
312
313 if (Errors)
314 return std::move(Errors);
315 return getFunctionSourceAfterReplacements(FD, DeclarationCleanups);
316}
317
318struct InsertionPoint {
319 const DeclContext *EnclosingNamespace = nullptr;
320 size_t Offset;
321};
322
323// Returns the range that should be deleted from declaration, which always
324// contains function body. In addition to that it might contain constructor
325// initializers.
326SourceRange getDeletionRange(const FunctionDecl *FD,
327 const syntax::TokenBuffer &TokBuf) {
328 auto DeletionRange = FD->getBody()->getSourceRange();
329 if (auto *CD = llvm::dyn_cast<CXXConstructorDecl>(FD)) {
330 // AST doesn't contain the location for ":" in ctor initializers. Therefore
331 // we find it by finding the first ":" before the first ctor initializer.
332 SourceLocation InitStart;
333 // Find the first initializer.
334 for (const auto *CInit : CD->inits()) {
335 // SourceOrder is -1 for implicit initializers.
336 if (CInit->getSourceOrder() != 0)
337 continue;
338 InitStart = CInit->getSourceLocation();
339 break;
340 }
341 if (InitStart.isValid()) {
342 auto Toks = TokBuf.expandedTokens(CD->getSourceRange());
343 // Drop any tokens after the initializer.
344 Toks = Toks.take_while([&TokBuf, &InitStart](const syntax::Token &Tok) {
345 return TokBuf.sourceManager().isBeforeInTranslationUnit(Tok.location(),
346 InitStart);
347 });
348 // Look for the first colon.
349 auto Tok =
350 llvm::find_if(llvm::reverse(Toks), [](const syntax::Token &Tok) {
351 return Tok.kind() == tok::colon;
352 });
353 assert(Tok != Toks.rend());
354 DeletionRange.setBegin(Tok->location());
355 }
356 }
357 return DeletionRange;
358}
359
360/// Moves definition of a function/method to an appropriate implementation file.
361///
362/// Before:
363/// a.h
364/// void foo() { return; }
365/// a.cc
366/// #include "a.h"
367///
368/// ----------------
369///
370/// After:
371/// a.h
372/// void foo();
373/// a.cc
374/// #include "a.h"
375/// void foo() { return; }
376class DefineOutline : public Tweak {
377public:
378 const char *id() const override;
379
380 bool hidden() const override { return false; }
381 llvm::StringLiteral kind() const override {
383 }
384 std::string title() const override {
385 return "Move function body to out-of-line";
386 }
387
388 bool prepare(const Selection &Sel) override {
389 SameFile = !isHeaderFile(Sel.AST->tuPath(), Sel.AST->getLangOpts());
390 Source = getSelectedFunction(Sel.ASTSelection.commonAncestor());
391
392 // Bail out if the selection is not a in-line function definition.
393 if (!Source || !Source->doesThisDeclarationHaveABody() ||
394 Source->isOutOfLine())
395 return false;
396
397 // Bail out if this is a function template or specialization, as their
398 // definitions need to be visible in all including translation units.
399 if (Source->getDescribedFunctionTemplate())
400 return false;
401 if (Source->getTemplateSpecializationInfo())
402 return false;
403
404 auto *MD = llvm::dyn_cast<CXXMethodDecl>(Source);
405 if (!MD) {
406 // Can't outline free-standing functions in the same file.
407 return !SameFile;
408 }
409
410 // Bail out in templated classes, as it is hard to spell the class name,
411 // i.e if the template parameter is unnamed.
412 if (MD->getParent()->isTemplated())
413 return false;
414
415 // The refactoring is meaningless for unnamed classes and namespaces,
416 // unless we're outlining in the same file
417 for (const DeclContext *DC = MD->getParent(); DC; DC = DC->getParent()) {
418 if (auto *ND = llvm::dyn_cast<NamedDecl>(DC)) {
419 if (ND->getDeclName().isEmpty() &&
420 (!SameFile || !llvm::dyn_cast<NamespaceDecl>(ND)))
421 return false;
422 }
423 }
424
425 // Note that we don't check whether an implementation file exists or not in
426 // the prepare, since performing disk IO on each prepare request might be
427 // expensive.
428 return true;
429 }
430
431 Expected<Effect> apply(const Selection &Sel) override {
432 const SourceManager &SM = Sel.AST->getSourceManager();
433 auto CCFile = SameFile ? Sel.AST->tuPath().str()
434 : getSourceFile(Sel.AST->tuPath(), Sel);
435 if (!CCFile)
436 return error("Couldn't find a suitable implementation file.");
437 assert(Sel.FS && "FS Must be set in apply");
438 auto Buffer = Sel.FS->getBufferForFile(*CCFile);
439 // FIXME: Maybe we should consider creating the implementation file if it
440 // doesn't exist?
441 if (!Buffer)
442 return llvm::errorCodeToError(Buffer.getError());
443 auto Contents = Buffer->get()->getBuffer();
444 auto InsertionPoint = getInsertionPoint(Contents, Sel);
445 if (!InsertionPoint)
446 return InsertionPoint.takeError();
447
448 auto FuncDef = getFunctionSourceCode(
449 Source, InsertionPoint->EnclosingNamespace, Sel.AST->getTokens(),
450 Sel.AST->getHeuristicResolver());
451 if (!FuncDef)
452 return FuncDef.takeError();
453
454 SourceManagerForFile SMFF(*CCFile, Contents);
455 const tooling::Replacement InsertFunctionDef(
456 *CCFile, InsertionPoint->Offset, 0, *FuncDef);
457 auto Effect = Effect::mainFileEdit(
458 SMFF.get(), tooling::Replacements(InsertFunctionDef));
459 if (!Effect)
460 return Effect.takeError();
461
462 tooling::Replacements HeaderUpdates(tooling::Replacement(
463 Sel.AST->getSourceManager(),
464 CharSourceRange::getTokenRange(*toHalfOpenFileRange(
465 SM, Sel.AST->getLangOpts(),
466 getDeletionRange(Source, Sel.AST->getTokens()))),
467 ";"));
468
469 if (Source->isInlineSpecified()) {
470 auto DelInline =
471 deleteTokensWithKind(Sel.AST->getTokens(), tok::kw_inline,
472 {Source->getBeginLoc(), Source->getLocation()});
473 if (!DelInline)
474 return DelInline.takeError();
475 HeaderUpdates = HeaderUpdates.merge(*DelInline);
476 }
477
478 if (SameFile) {
479 tooling::Replacements &R = Effect->ApplyEdits[*CCFile].Replacements;
480 R = R.merge(HeaderUpdates);
481 } else {
482 auto HeaderFE = Effect::fileEdit(SM, SM.getMainFileID(), HeaderUpdates);
483 if (!HeaderFE)
484 return HeaderFE.takeError();
485 Effect->ApplyEdits.try_emplace(HeaderFE->first,
486 std::move(HeaderFE->second));
487 }
488 return std::move(*Effect);
489 }
490
491 // Returns the most natural insertion point for \p QualifiedName in \p
492 // Contents. This currently cares about only the namespace proximity, but in
493 // feature it should also try to follow ordering of declarations. For example,
494 // if decls come in order `foo, bar, baz` then this function should return
495 // some point between foo and baz for inserting bar.
496 // FIXME: The selection can be made smarter by looking at the definition
497 // locations for adjacent decls to Source. Unfortunately pseudo parsing in
498 // getEligibleRegions only knows about namespace begin/end events so we
499 // can't match function start/end positions yet.
500 llvm::Expected<InsertionPoint> getInsertionPoint(llvm::StringRef Contents,
501 const Selection &Sel) {
502 // If the definition goes to the same file and there is a namespace,
503 // we should (and, in the case of anonymous namespaces, need to)
504 // put the definition into the original namespace block.
505 if (SameFile) {
506 auto *Klass = Source->getDeclContext()->getOuterLexicalRecordContext();
507 if (!Klass)
508 return error("moving to same file not supported for free functions");
509 const SourceLocation EndLoc = Klass->getBraceRange().getEnd();
510 const auto &TokBuf = Sel.AST->getTokens();
511 auto Tokens = TokBuf.expandedTokens();
512 auto It = llvm::lower_bound(
513 Tokens, EndLoc, [](const syntax::Token &Tok, SourceLocation EndLoc) {
514 return Tok.location() < EndLoc;
515 });
516 while (It != Tokens.end()) {
517 if (It->kind() != tok::semi) {
518 ++It;
519 continue;
520 }
521 unsigned Offset = Sel.AST->getSourceManager()
522 .getDecomposedLoc(It->endLocation())
523 .second;
524 return InsertionPoint{Klass->getEnclosingNamespaceContext(), Offset};
525 }
526 return error(
527 "failed to determine insertion location: no end of class found");
528 }
529
530 auto Region = getEligiblePoints(
531 Contents, Source->getQualifiedNameAsString(), Sel.AST->getLangOpts());
532
533 assert(!Region.EligiblePoints.empty());
534 auto Offset = positionToOffset(Contents, Region.EligiblePoints.back());
535 if (!Offset)
536 return Offset.takeError();
537
538 auto TargetContext =
539 findContextForNS(Region.EnclosingNamespace, Source->getDeclContext());
540 if (!TargetContext)
541 return error("define outline: couldn't find a context for target");
542
543 return InsertionPoint{*TargetContext, *Offset};
544 }
545
546private:
547 const FunctionDecl *Source = nullptr;
548 bool SameFile = false;
549};
550
551REGISTER_TWEAK(DefineOutline)
552
553} // namespace
554} // namespace clangd
555} // namespace clang
BindArgumentKind Kind
size_t Offset
std::optional< std::string > EnclosingNamespace
StringRef FileName
#define REGISTER_TWEAK(Subclass)
Definition: Tweak.h:129
std::optional< SourceRange > toHalfOpenFileRange(const SourceManager &SM, const LangOptions &LangOpts, SourceRange R)
Turns a token range into a half-open range and checks its correctness.
Definition: SourceCode.cpp:430
std::string getQualification(ASTContext &Context, const DeclContext *DestContext, SourceLocation InsertionPoint, const NamedDecl *ND)
Gets the nested name specifier necessary for spelling ND in DestContext, at InsertionPoint.
Definition: AST.cpp:660
void findExplicitReferences(const Stmt *S, llvm::function_ref< void(ReferenceLoc)> Out, const HeuristicResolver *Resolver)
Recursively traverse S and report all references explicitly written in the code.
llvm::Error error(std::error_code EC, const char *Fmt, Ts &&... Vals)
Definition: Logger.h:79
EligibleRegion getEligiblePoints(llvm::StringRef Code, llvm::StringRef FullyQualifiedName, const LangOptions &LangOpts)
Returns most eligible region to insert a definition for FullyQualifiedName in the Code.
llvm::Expected< size_t > positionToOffset(llvm::StringRef Code, Position P, bool AllowColumnsBeyondLineLength)
Turn a [line, column] pair into an offset in Code.
Definition: SourceCode.cpp:173
std::optional< Path > getCorrespondingHeaderOrSource(PathRef OriginalFile, llvm::IntrusiveRefCntPtr< llvm::vfs::FileSystem > VFS)
Given a header file, returns the best matching source file, and vice visa.
bool isHeaderFile(llvm::StringRef FileName, std::optional< LangOptions > LangOpts)
Infers whether this is a header from the FileName and LangOpts (if presents).
===– Representation.cpp - ClangDoc Representation --------—*- C++ -*-===//
static const llvm::StringLiteral REFACTOR_KIND
Definition: Protocol.h:1071