This is an archive of the discontinued LLVM Phabricator instance.

[clangd] Add support to extract method for ExtractFunction Tweak
ClosedPublic

Authored by FabioRS on Mar 29 2022, 6:06 PM.

Details

Summary

I miss more automatically refactoring functions when working with already running code, so I am making some small addition that I hope help more people.

This works by checking if the function is a method (CXXMethodDecl), then collecting information about the function that the code is being extracted, looking for the declaration if it is out-of-line, creating the declaration if it is necessary and putting the extracted function as a class-method.

This is my first code review request, sorry if I did something wrong.

Diff Detail

Event Timeline

FabioRS created this revision.Mar 29 2022, 6:06 PM
Herald added a project: Restricted Project. · View Herald TranscriptMar 29 2022, 6:06 PM
FabioRS requested review of this revision.Mar 29 2022, 6:06 PM

Thanks for doing this, it looks great!

A few comments on:

  • a few on cases that I think aren't handled quite right
  • structure of the new code
  • structure of the *old* code: names etc that no longer fit after these changes

There's also the ugliness around merging edits/tweak effects - this has come up in a couple of other checks and we should have a nicer way to do it, sorry it's not there yet :-)

clang-tools-extra/clangd/refactor/tweaks/ExtractFunction.cpp
414

nit: these are *specifiers* rather than attributes

428

and these are *qualifiers* rather than attributes

438

this isn't actually rendering namespaces, right? just classes

442

What if this class is nested?
e.g.

class Foo { class Bar { int baz(); }; };
int Foo::Bar::baz() { ... }

we need to produce void Foo::Bar::extracted() { ... }

I think the easiest thing here is just to save the NestedNameSpecifier* from the original out-of-line definition (getQualifier()), this represents the syntax Foo::Bar:: as written. Then you can just print() it.

464

the naming of this family of functions, and the irregular reuse between them doesn't sit quite right with me, especially the way it suggests "inline definition" is a special case (when for plain functions it's the only case).

What about renderDeclaration(FunctionDeclKind K, DeclContext &Enclosing, const SourceManager &SM);

with enum FunctionDeclKind { InlineDefinition, ForwardDeclaration, OutOfLineDefinition }?,

465

here you're excluding the specifiers, which is correct for static but incorrect for constexpr :-(

473

For out-of-line defined methods, this is the wrong DC, which may lead to the type being incorrectly qualified. The DC should rather be the enclosing class.

Similarly, renderParametersForDefinition uses EnclosingFuncContext too - instead it should take the EnclosingFuncContext as a parameter. (And be renamed renderParametersForDeclaration, since it's no longer just for definitions)

761

Maybe a high-level comment:

For methods defined out-of-line, the extracted method will also be out-of-line. The new declaration/definition will each be next to the existing ones.
Methods defined inline are treated very similarly to plain functions.

768

may want to propagate consteval too
maybe instead of bool Constexpr, store ConstexprSpecKind Constexpr = Method->getConstexprKind()?

769

nit: why is this called ContextConst rather than just const? We can already decide at this point that the new function will be const.

774

I think we should call this EnclosingClass rather than ParentContext, as we only set it/care about it when it's a class, never when it's a namespace

775

There's some redundancy between DeclarationPoint and OutOfLine.

Can we call this one ForwardDeclarationPoint, only set it if the method is out-of-line? Then we can get rid of OutOfLine and just check the presence/absence of ForwardDeclarationPoint.

775

it might be more appropriate to place this elsewhere in the class, e.g. in the private section, among the methods even if we're extracting from a constructor etc. We now have clangd/refactor/InsertionPoint.h to help with this.

I don't think you should do this now (keep the scope small), but might be worth a FIXME

785–786

now that we have multiple insertions, NewFunction::InsertionPoint should be called DefinitionPoint instead

FabioRS updated this revision to Diff 419290.Mar 30 2022, 4:58 PM
FabioRS marked 14 inline comments as done.

I am not sure about the LangOptions object, the NestedNameSpecifier needs it to correctly print the nested classes-name and not sure about the getEnclosing() too.

I can not run the test the consteval, so it is not in the diff.

TestTU failed to build (suppress with /*error-ok*/): 
[1:4-1:13] unknown type name 'consteval'; did you mean 'constexpr'?, fixes: {change 'consteval' to 'constexpr' {1:4-1:13 => "constexpr"}}

For code:

    consteval void SomeClass::f() const {
      int x;
    }
FabioRS updated this revision to Diff 419296.Mar 30 2022, 5:21 PM

Full patch

Thanks! I'll look at the changes in the morning, just wanted to mention one thing

I can not run the test the consteval, so it is not in the diff.

TestTU failed to build (suppress with /*error-ok*/): 
[1:4-1:13] unknown type name 'consteval'; did you mean 'constexpr'?, fixes: {change 'consteval' to 'constexpr' {1:4-1:13 => "constexpr"}}

It's a c++20 feature, so you'll need to add something like ExtraArgs.push_back("-std=c++20");.

Though if there's no real difference between handling constexpr vs consteval, up to you whether it's worth a separate test.

Thanks! I'll look at the changes in the morning, just wanted to mention one thing

I can not run the test the consteval, so it is not in the diff.

TestTU failed to build (suppress with /*error-ok*/): 
[1:4-1:13] unknown type name 'consteval'; did you mean 'constexpr'?, fixes: {change 'consteval' to 'constexpr' {1:4-1:13 => "constexpr"}}

It's a c++20 feature, so you'll need to add something like ExtraArgs.push_back("-std=c++20");.

Though if there's no real difference between handling constexpr vs consteval, up to you whether it's worth a separate test.

Thank you!
I think it is interesting to have the test to keep the consteval branch covered. I think maybe there will be more things to change, so I can send the test case for consteval tomorrow with the others changes.

clang-tools-extra/clangd/refactor/tweaks/ExtractFunction.cpp
473

It is only for out-of-line methods? I put this verification in the getEnclosing() of the new diff.

OK, I think the main remaining thing is enclosing contexts... (sorry, I wasn't thinking clearly about these in the last round, and I think existing code gets this slightly wrong too).

Each declaration has potentially *two* contexts, semantic and syntactic, and the distinction is relevant here.

namespace ns {
class A {
  void f(); // SemanticDC = A, SyntacticDC = A
};
void A::f() { } // SemanticDC = A, SyntacticDC = ns
}

To determine how to spell different parts of the function, different rules apply to different parts of the declaration:

  • return type and function name needs to be relative to the SyntacticDC
  • parameters and function body are relative to the SemanticDC

I think renderDeclaration() should take SemanticDC and SyntacticDC as parameters, and NewFunction should have SemanticDC, SyntacticDC, ForwardDeclarationSyntacticDC as fields. (By definition, the forward declaration and the definition have the same semantic DC).

To summarize, something like:

struct NewFunction {
  DeclContext *SemanticDC;
  DeclContext *SyntacticDC;
  DeclContext *ForwardDeclarationSyntacticDC;
}
void renderDeclaration(FunctionDeclKind, DeclContext& SemanticDC, DeclContext &SyntacticDC, ...) {
  ... printType(ReturnType, SyntacticDC), ..., renderParametersForDefinition(SemanticDC)...
}
void createFunctionDefinition(...) {
   renderDeclaration(..., SemanticDC, SyntacticDC, ...);
}
void createForwardDeclaration(...) {
  renderDeclaration(..., SemanticDC, ForwardDeclarationSyntacticDC, ...);
}
clang-tools-extra/clangd/refactor/tweaks/ExtractFunction.cpp
352

you only ever use the beginning of this range (and it's an insertion point, so having a range doesn't make much sense). Change to SourceLocation?

354

nit: const (the AST isn't great about const-correctness, but should be OK here)

Maybe NestedNameSpec -> DefinitionQualifier?

361

this needs a more specific name - see overall comment on the patch

366

nit: pointer rather than reference, reference members have surprising effects and we don't have any here so far

473

you don't need separate renderDeclaration() and renderDefinition() functions: the FunctionDeclKind tells you whether this is a definition or not.

(And naming-wise, definitions are just a special type of declaration)

473

functions can also be defined in a different syntactic context than they are declared:

namespace ns { void foo(); }
void ns::foo() {}

This is rarer and I suspect we get this case wrong today.

847

nit: createForwardDeclaration

FabioRS updated this revision to Diff 419616.EditedMar 31 2022, 9:15 PM
FabioRS marked 6 inline comments as done.
FabioRS set the repository for this revision to rG LLVM Github Monorepo.

Thanks!

namespace ns { int y = 1; void foo(); }
void ns::foo() {[[int x; y++; ]]}
namespace ns { int y = 1; void foo(); }
void extracted() {
int x; y++;
}
void ns::foo() {extracted(); }

This is not working, but I think it's not the scope of this review request, what do you think?
Later today I will take a look at the namespace context problem to verify how much and what code fixes it.

(See the tests line 478)

        class SomeClass {
          static ns::A::C::RType extracted();
static C::RType f();
        };

This is valid code but the syntactic looks strange with all the namespaces/classes, I tested various contexts but was not fixed, do you have some ideia for me to do?

FabioRS updated this revision to Diff 419868.Apr 1 2022, 3:07 PM
FabioRS marked an inline comment as done.

I did some simple refactor on the code I did for the function-method and it fixed the namespace syntactic problem too. I am not sure if it expands the pull request scope.

nridge added a subscriber: nridge.Apr 3 2022, 5:47 PM
sammccall accepted this revision.Apr 4 2022, 7:03 AM

Thanks, this looks great! Just a couple of nits left.
Appreciate you fixing the out-of-line ns::f() case too. It's easier to understand the fixed logic than the broken logic.

I guess you don't have commit access yet, I can land this for you if you like. Let me know your preferred name/email for attribution.

clang-tools-extra/clangd/refactor/tweaks/ExtractFunction.cpp
765

You're setting/resetting these in lots of different places, but no need for that:

SyntacticDC = EnclosingFunction->getLexicalDeclContext()
SemanticDC = getDeclContext();

and set ForwardDeclarationSyntacticDC in captureMethodInfo(), leave it null if this isn't a method.

770

this does one check, and is idiomatic in LLVM:

if (const auto *Method = llvm::dyn_cast<...>(...))
  captureMethodInfo(...);
This revision is now accepted and ready to land.Apr 4 2022, 7:03 AM
FabioRS marked 2 inline comments as done.Apr 4 2022, 3:48 PM

Thanks, this looks great! Just a couple of nits left.
Appreciate you fixing the out-of-line ns::f() case too. It's easier to understand the fixed logic than the broken logic.

I guess you don't have commit access yet, I can land this for you if you like. Let me know your preferred name/email for attribution.

Thanks! I'm glad to help.

I don't have commit access. You can land it for me please, thanks!
Name: Fabio Rossini Sluzala
e-mail: fabio3rs@gmail.com

FabioRS added inline comments.Apr 4 2022, 3:51 PM
clang-tools-extra/clangd/refactor/tweaks/ExtractFunction.cpp
765

Thanks!
The out-of-line ns:f() needs the ForwardDeclarationSyntacticDC too, I think inside the branch if (ExtZone.EnclosingFunction->isOutOfLine()) is the place to put it, I will submit a diff.

FabioRS updated this revision to Diff 420336.Apr 4 2022, 4:02 PM

Thanks!

I removed the excess setting/resseting of the SyntacticDC and SemanticDC

ExtractedFunc.SyntacticDC =
    ExtZone.EnclosingFunction->getLexicalDeclContext();
ExtractedFunc.SemanticDC = ExtZone.EnclosingFunction->getDeclContext();
if (const auto *Method =
        llvm::dyn_cast<CXXMethodDecl>(ExtZone.EnclosingFunction))
  captureMethodInfo(ExtractedFunc, Method);

I place the ExtractedFunc.ForwardDeclarationSyntacticDC = ExtractedFunc.SemanticDC inside the if (ExtZone.EnclosingFunction->isOutOfLine()) branch since the out-of-line ns:f() needs it too. What do you think?

You're right about the out-of-line function case. Current version looks great!
I had to rework the logic around merging the edits a little, it wasn't quite correct and was hitting assertions (not sure if you were seeing these locally, but phabricator is not currently running the clangd tests properly).

Thanks again, landing now.

You're right about the out-of-line function case. Current version looks great!
I had to rework the logic around merging the edits a little, it wasn't quite correct and was hitting assertions (not sure if you were seeing these locally, but phabricator is not currently running the clangd tests properly).

Thanks again, landing now.

Thanks.

Here the tests are passing, maybe something is missing in my compilation setup... There are these unsupported tests, but I was thinking they are for another architeture.

ninja check-clangd
[1/2] Running the Clangd regression tests
llvm-lit: /homessddata/Projects/llvm-project/llvm/utils/lit/lit/llvm/config.py:438: note: using clang: /homessddata/Projects/llvm-project/build/bin/clang

Testing Time: 3.26s
  Unsupported:    6
  Passed     : 1129

You are welcome. Thank you for your patience and time, I am glad to help.