Skip to content

Commit 955c223

Browse files
authored
[clangd] Allow "move function body out-of-line" in non-header files (#69704)
Moving the body of member functions out-of-line makes sense for classes defined in implementation files too.
1 parent e8ff03b commit 955c223

File tree

2 files changed

+164
-59
lines changed

2 files changed

+164
-59
lines changed

clang-tools-extra/clangd/refactor/tweaks/DefineOutline.cpp

Lines changed: 93 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -179,14 +179,11 @@ deleteTokensWithKind(const syntax::TokenBuffer &TokBuf, tok::TokenKind Kind,
179179
// looked up in the context containing the function/method.
180180
// FIXME: Drop attributes in function signature.
181181
llvm::Expected<std::string>
182-
getFunctionSourceCode(const FunctionDecl *FD, llvm::StringRef TargetNamespace,
182+
getFunctionSourceCode(const FunctionDecl *FD, const DeclContext *TargetContext,
183183
const syntax::TokenBuffer &TokBuf,
184184
const HeuristicResolver *Resolver) {
185185
auto &AST = FD->getASTContext();
186186
auto &SM = AST.getSourceManager();
187-
auto TargetContext = findContextForNS(TargetNamespace, FD->getDeclContext());
188-
if (!TargetContext)
189-
return error("define outline: couldn't find a context for target");
190187

191188
llvm::Error Errors = llvm::Error::success();
192189
tooling::Replacements DeclarationCleanups;
@@ -216,7 +213,7 @@ getFunctionSourceCode(const FunctionDecl *FD, llvm::StringRef TargetNamespace,
216213
}
217214
const NamedDecl *ND = Ref.Targets.front();
218215
const std::string Qualifier =
219-
getQualification(AST, *TargetContext,
216+
getQualification(AST, TargetContext,
220217
SM.getLocForStartOfFile(SM.getMainFileID()), ND);
221218
if (auto Err = DeclarationCleanups.add(
222219
tooling::Replacement(SM, Ref.NameLoc, 0, Qualifier)))
@@ -232,7 +229,7 @@ getFunctionSourceCode(const FunctionDecl *FD, llvm::StringRef TargetNamespace,
232229
if (const auto *Destructor = llvm::dyn_cast<CXXDestructorDecl>(FD)) {
233230
if (auto Err = DeclarationCleanups.add(tooling::Replacement(
234231
SM, Destructor->getLocation(), 0,
235-
getQualification(AST, *TargetContext,
232+
getQualification(AST, TargetContext,
236233
SM.getLocForStartOfFile(SM.getMainFileID()),
237234
Destructor))))
238235
Errors = llvm::joinErrors(std::move(Errors), std::move(Err));
@@ -319,29 +316,9 @@ getFunctionSourceCode(const FunctionDecl *FD, llvm::StringRef TargetNamespace,
319316
}
320317

321318
struct InsertionPoint {
322-
std::string EnclosingNamespace;
319+
const DeclContext *EnclosingNamespace = nullptr;
323320
size_t Offset;
324321
};
325-
// Returns the most natural insertion point for \p QualifiedName in \p Contents.
326-
// This currently cares about only the namespace proximity, but in feature it
327-
// should also try to follow ordering of declarations. For example, if decls
328-
// come in order `foo, bar, baz` then this function should return some point
329-
// between foo and baz for inserting bar.
330-
llvm::Expected<InsertionPoint> getInsertionPoint(llvm::StringRef Contents,
331-
llvm::StringRef QualifiedName,
332-
const LangOptions &LangOpts) {
333-
auto Region = getEligiblePoints(Contents, QualifiedName, LangOpts);
334-
335-
assert(!Region.EligiblePoints.empty());
336-
// FIXME: This selection can be made smarter by looking at the definition
337-
// locations for adjacent decls to Source. Unfortunately pseudo parsing in
338-
// getEligibleRegions only knows about namespace begin/end events so we
339-
// can't match function start/end positions yet.
340-
auto Offset = positionToOffset(Contents, Region.EligiblePoints.back());
341-
if (!Offset)
342-
return Offset.takeError();
343-
return InsertionPoint{Region.EnclosingNamespace, *Offset};
344-
}
345322

346323
// Returns the range that should be deleted from declaration, which always
347324
// contains function body. In addition to that it might contain constructor
@@ -409,14 +386,9 @@ class DefineOutline : public Tweak {
409386
}
410387

411388
bool prepare(const Selection &Sel) override {
412-
// Bail out if we are not in a header file.
413-
// FIXME: We might want to consider moving method definitions below class
414-
// definition even if we are inside a source file.
415-
if (!isHeaderFile(Sel.AST->getSourceManager().getFilename(Sel.Cursor),
416-
Sel.AST->getLangOpts()))
417-
return false;
418-
389+
SameFile = !isHeaderFile(Sel.AST->tuPath(), Sel.AST->getLangOpts());
419390
Source = getSelectedFunction(Sel.ASTSelection.commonAncestor());
391+
420392
// Bail out if the selection is not a in-line function definition.
421393
if (!Source || !Source->doesThisDeclarationHaveABody() ||
422394
Source->isOutOfLine())
@@ -429,19 +401,24 @@ class DefineOutline : public Tweak {
429401
if (Source->getTemplateSpecializationInfo())
430402
return false;
431403

432-
if (auto *MD = llvm::dyn_cast<CXXMethodDecl>(Source)) {
433-
// Bail out in templated classes, as it is hard to spell the class name,
434-
// i.e if the template parameter is unnamed.
435-
if (MD->getParent()->isTemplated())
436-
return false;
437-
438-
// The refactoring is meaningless for unnamed classes and definitions
439-
// within unnamed namespaces.
440-
for (const DeclContext *DC = MD->getParent(); DC; DC = DC->getParent()) {
441-
if (auto *ND = llvm::dyn_cast<NamedDecl>(DC)) {
442-
if (ND->getDeclName().isEmpty())
443-
return false;
444-
}
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;
445422
}
446423
}
447424

@@ -453,8 +430,8 @@ class DefineOutline : public Tweak {
453430

454431
Expected<Effect> apply(const Selection &Sel) override {
455432
const SourceManager &SM = Sel.AST->getSourceManager();
456-
auto CCFile = getSourceFile(Sel.AST->tuPath(), Sel);
457-
433+
auto CCFile = SameFile ? Sel.AST->tuPath().str()
434+
: getSourceFile(Sel.AST->tuPath(), Sel);
458435
if (!CCFile)
459436
return error("Couldn't find a suitable implementation file.");
460437
assert(Sel.FS && "FS Must be set in apply");
@@ -464,8 +441,7 @@ class DefineOutline : public Tweak {
464441
if (!Buffer)
465442
return llvm::errorCodeToError(Buffer.getError());
466443
auto Contents = Buffer->get()->getBuffer();
467-
auto InsertionPoint = getInsertionPoint(
468-
Contents, Source->getQualifiedNameAsString(), Sel.AST->getLangOpts());
444+
auto InsertionPoint = getInsertionPoint(Contents, Sel);
469445
if (!InsertionPoint)
470446
return InsertionPoint.takeError();
471447

@@ -499,17 +475,77 @@ class DefineOutline : public Tweak {
499475
HeaderUpdates = HeaderUpdates.merge(*DelInline);
500476
}
501477

502-
auto HeaderFE = Effect::fileEdit(SM, SM.getMainFileID(), HeaderUpdates);
503-
if (!HeaderFE)
504-
return HeaderFE.takeError();
505-
506-
Effect->ApplyEdits.try_emplace(HeaderFE->first,
507-
std::move(HeaderFE->second));
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+
}
508488
return std::move(*Effect);
509489
}
510490

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+
511546
private:
512547
const FunctionDecl *Source = nullptr;
548+
bool SameFile = false;
513549
};
514550

515551
REGISTER_TWEAK(DefineOutline)

clang-tools-extra/clangd/unittests/tweaks/DefineOutlineTests.cpp

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,47 @@ TWEAK_TEST(DefineOutline);
1919

2020
TEST_F(DefineOutlineTest, TriggersOnFunctionDecl) {
2121
FileName = "Test.cpp";
22-
// Not available unless in a header file.
22+
// Not available for free function unless in a header file.
2323
EXPECT_UNAVAILABLE(R"cpp(
2424
[[void [[f^o^o]]() [[{
2525
return;
2626
}]]]])cpp");
2727

28+
// Available in soure file.
29+
EXPECT_AVAILABLE(R"cpp(
30+
struct Foo {
31+
void f^oo() {}
32+
};
33+
)cpp");
34+
35+
// Available within named namespace in source file.
36+
EXPECT_AVAILABLE(R"cpp(
37+
namespace N {
38+
struct Foo {
39+
void f^oo() {}
40+
};
41+
} // namespace N
42+
)cpp");
43+
44+
// Available within anonymous namespace in source file.
45+
EXPECT_AVAILABLE(R"cpp(
46+
namespace {
47+
struct Foo {
48+
void f^oo() {}
49+
};
50+
} // namespace
51+
)cpp");
52+
53+
// Not available for out-of-line method.
54+
EXPECT_UNAVAILABLE(R"cpp(
55+
class Bar {
56+
void baz();
57+
};
58+
59+
[[void [[Bar::[[b^a^z]]]]() [[{
60+
return;
61+
}]]]])cpp");
62+
2863
FileName = "Test.hpp";
2964
// Not available unless function name or fully body is selected.
3065
EXPECT_UNAVAILABLE(R"cpp(
@@ -100,7 +135,7 @@ TEST_F(DefineOutlineTest, TriggersOnFunctionDecl) {
100135
};
101136
)cpp");
102137

103-
// Not available on definitions within unnamed namespaces
138+
// Not available on definitions in header file within unnamed namespaces
104139
EXPECT_UNAVAILABLE(R"cpp(
105140
namespace {
106141
struct Foo {
@@ -349,6 +384,40 @@ TEST_F(DefineOutlineTest, ApplyTest) {
349384
}
350385
}
351386

387+
TEST_F(DefineOutlineTest, InCppFile) {
388+
FileName = "Test.cpp";
389+
390+
struct {
391+
llvm::StringRef Test;
392+
llvm::StringRef ExpectedSource;
393+
} Cases[] = {
394+
{
395+
R"cpp(
396+
namespace foo {
397+
namespace {
398+
struct Foo { void ba^r() {} };
399+
struct Bar { void foo(); };
400+
void Bar::foo() {}
401+
}
402+
}
403+
)cpp",
404+
R"cpp(
405+
namespace foo {
406+
namespace {
407+
struct Foo { void bar() ; };void Foo::bar() {}
408+
struct Bar { void foo(); };
409+
void Bar::foo() {}
410+
}
411+
}
412+
)cpp"},
413+
};
414+
415+
for (const auto &Case : Cases) {
416+
SCOPED_TRACE(Case.Test);
417+
EXPECT_EQ(apply(Case.Test, nullptr), Case.ExpectedSource);
418+
}
419+
}
420+
352421
TEST_F(DefineOutlineTest, HandleMacros) {
353422
llvm::StringMap<std::string> EditedFiles;
354423
ExtraFiles["Test.cpp"] = "";

0 commit comments

Comments
 (0)