Skip to content

[clang-tidy] Add bugprone-move-shared-pointer-contents check. #67467

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 35 commits into
base: main
Choose a base branch
from

Conversation

pizzud
Copy link

@pizzud pizzud commented Sep 26, 2023

This check detects moves of the contents of a shared pointer rather than the pointer itself. Other code with a reference to the shared pointer is probably not expecting the move.

The set of shared pointer classes is configurable via options to allow individual projects to cover additional types.

Copy link
Contributor

@5chmidti 5chmidti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed a problem with your matcher, so I reviewed the rest of it while I was at it.

The problem is that you do not consider a type-dependent std::shared_ptr<T> and the following test case fails:

template <typename T>
void dependentType() {
  std::shared_ptr<T> p;
  T y = std::move(*p);
  // CHECK-FIXES: *std::move(p)
}
// CHECK-MESSAGES: :[[@LINE-3]]:11: warning: don't move the contents out of a shared pointer, as other accessors expect them to remain in a determinate state [bugprone-move-shared-pointer-contents]

In this case, your callee is no longer a functionDecl but an unresolvedLookupExpr and the * operator is a UnaryOperator instead of a cxxOperatorCallExpr.

Otherwise, looks like a good check to have.

Copy link

github-actions bot commented Dec 1, 2023

✅ With the latest revision this PR passed the C/C++ code formatter.

@pizzud
Copy link
Author

pizzud commented Dec 1, 2023

I noticed a problem with your matcher, so I reviewed the rest of it while I was at it.

The problem is that you do not consider a type-dependent std::shared_ptr<T> and the following test case fails:

template <typename T>
void dependentType() {
  std::shared_ptr<T> p;
  T y = std::move(*p);
  // CHECK-FIXES: *std::move(p)
}
// CHECK-MESSAGES: :[[@LINE-3]]:11: warning: don't move the contents out of a shared pointer, as other accessors expect them to remain in a determinate state [bugprone-move-shared-pointer-contents]

In this case, your callee is no longer a functionDecl but an unresolvedLookupExpr and the * operator is a UnaryOperator instead of a cxxOperatorCallExpr.

Otherwise, looks like a good check to have.

I'm really struggling with this, and also with getting the git history right after three months oops. The UnaryOperator is matchable without much difficulty, but there appears to be no way to introspect the UnresolvedLookupExpr to ensure I'm matching calls to std::move.

@5chmidti
Copy link
Contributor

5chmidti commented Dec 4, 2023

but there appears to be no way to introspect the UnresolvedLookupExpr to ensure I'm matching calls to std::move

Try searching for unresolvedLookupExpr & UnresolvedLookupExpr in the clang-tidy directory. There is a high probability that another check has done something related/similar.

callee(unresolvedLookupExpr(hasAnyDeclaration(
namedDecl(hasUnderlyingDecl(hasName("::std::move"))))))),

And there is OverloadExpr::decls, which UnresolvedLookupExpr inherits (https://clang.llvm.org/doxygen/classclang_1_1UnresolvedLookupExpr.html).

Copy link
Member

@PiotrZSL PiotrZSL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few fixes needed, but looks you are on good road.

@pizzud
Copy link
Author

pizzud commented Dec 4, 2023

Try searching for unresolvedLookupExpr & UnresolvedLookupExpr in the clang-tidy directory. There is a high probability that another check has done something related/similar.

callee(unresolvedLookupExpr(hasAnyDeclaration(
namedDecl(hasUnderlyingDecl(hasName("::std::move"))))))),

Thanks, this was super helpful and exactly what I was looking for!

I think the missing insight was that I could go from the lookup -> decls.

Copy link
Member

@PiotrZSL PiotrZSL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few issues with unaryOperator left, after that will be fixed, it would look fine.

@pizzud
Copy link
Author

pizzud commented Jan 5, 2024

Not sure how to squash properly; I tried rebasing but all the commits on the main branch snuck in to the history and I'm quite new to git. If it would be preferred I can store this off as a patch and open up a new PR. Sorry for all the hassle with the merges!

@EugeneZelenko
Copy link
Contributor

You need to do interactive rebase:

git fetch --prune # get latest main and remove local copies deleted branch 
git rebase origin/main # rebase from main
git rebase -i origin/main # interactive rebase, just use fixup command
git rebase --force origin <your branch name> # push updated branch

@pizzud pizzud force-pushed the shared-ptr branch 2 times, most recently from 97323d0 to ab88688 Compare January 8, 2024 19:44
@pizzud
Copy link
Author

pizzud commented Jan 8, 2024

A coworker was able to help me get this working by rebasing against upstream, so I think we're good to go at this point. Thanks for all the feedback!

@denzor200
Copy link

Wouldn't that be a runtime check, and thus not something clang-tidy can do without significant data flow analysis effort?

I'm not experienced enough in clang-tidy internals, but I believe it will not be a serious challenge to check for having assert(sp.use_count() == 1) one line before *sp being moved.

I suppose everybody agree that this snippet

assert(sp.use_count() == 1);
auto y = std::move(*sp);

..is better than that one:

// NOLINTNEXTLINE(clang-tidy-bugprone-move-shared-pointer-contents)
auto y = std::move(*sp);

@vbvictor
Copy link
Contributor

vbvictor commented Jun 3, 2025

@pizzud, you wish to continue work on this PR? 21th release of LLVM would be in late July, we have time to prepare and merge this check before the release. Also, It's okay to not cover all cases in the first iteration, some improvements can be added in later patches.

Windows build is failing most likely to absent of "-fno-delayed-template-parsing" flag in test command, look for it in other tests for reference.

@pizzud
Copy link
Author

pizzud commented Jun 13, 2025

@denzor2000 I agree it's better and technically possible within clang-tidy, but I am not sure it's simple to do. I'd like to get this in as-is.

@vbvictor
Thanks for the MSVC tip! I am still interested in this, and I will spend some time to get the git history back in shape and publish my contents.

@vbvictor
Copy link
Contributor

I agree that modelling assert(sp.use_count() == 1); is not easy in clang-tidy itself. In one of my checks I needed to check that two declStmt are placed one after another in compoundStmt and I ended up manually traversing all stmt within compoundStmt - not a very pleasant solution.
clang static analyzer has built-in functionality for modelling assert() and other macros, but clang-tidy doesn't

@vbvictor
Copy link
Contributor

Please ping me when you're done working and want to test CI build
I need to approve every CI build/format run... not very convenient.

pizzud and others added 3 commits June 13, 2025 13:07
…pointer-contents.rst

Co-authored-by: EugeneZelenko <eugene.zelenko@gmail.com>
@pizzud
Copy link
Author

pizzud commented Jun 13, 2025

Give it a go?

}
}

if (Call == nullptr && !Call->getBeginLoc().isValid()) {
Copy link
Contributor

@vbvictor vbvictor Jun 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be Call != nullptr or simply Call &&?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither. It should be || instead of &&. The somewhat scary (but unsurprising part since I'm not sure how you would actually get here) is that all the unit tests passed either way.


if (Call == nullptr && !Call->getBeginLoc().isValid()) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using braces for single if-stmt

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -148,6 +148,12 @@ New checks
Finds potentially erroneous calls to ``reset`` method on smart pointers when
the pointee type also has a ``reset`` method.

- New :doc:`bugprone-move-shared-pointer-contents
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please place in alphabetical order by check name

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor

@vbvictor vbvictor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please run clang-tidy over your file and fix the errors, clangd can detect them, too.

A semicolon-separated list of class names that should be treated as
shared pointers. Classes are resolved through aliases, so any alias
to the defined classes will be considered. Default is
`::std::shared_ptr;::boost::shared_pointer`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boost has ::boost::shared_ptr, not ::boost::shared_pointer

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, done. Fixed in the actual checker as well.

#include "../ClangTidyCheck.h"
#include "../utils/Matchers.h"
#include "../utils/OptionsUtils.h"
#include "clang/AST/ASTContext.h"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused include?

Suggested change
#include "clang/AST/ASTContext.h"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@vbvictor
Copy link
Contributor

vbvictor commented Jun 21, 2025

I played with matchers a bit, one hasDescendant may be eliminated by:

diff --git a/clang-tools-extra/clang-tidy/bugprone/MoveSharedPointerContentsCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/MoveSharedPointerContentsCheck.cpp
index 4d5c25bf9f27..ab5e13fbb9cc 100644
--- a/clang-tools-extra/clang-tidy/bugprone/MoveSharedPointerContentsCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/MoveSharedPointerContentsCheck.cpp
@@ -70,12 +70,11 @@ void MoveSharedPointerContentsCheck::registerMatchers(MatchFinder *Finder) {
           hasArgument(
               0, unaryOperator(
                      hasOperatorName("*"),
-                     hasUnaryOperand(allOf(
-                         hasDescendant(declRefExpr(hasType(qualType(
-                             isSharedPointer(matchers::matchesAnyListedName(
-                                 SharedPointerClasses)))))),
-                         cxxMemberCallExpr(
-                             callee(cxxMethodDecl(hasName("get")))))))))
+                     hasUnaryOperand(cxxMemberCallExpr(
+                         callee(cxxMethodDecl(hasName("get"))),
+                         on(hasType(qualType(isSharedPointer(
+                             matchers::matchesAnyListedName(
+                                 SharedPointerClasses))))))))))
           .bind("get_call")));

All the tests seem to pass on ubuntu, couldn't test on windows

Copy link
Author

@pizzud pizzud left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I got the clang-tidy issues sorted out.

A semicolon-separated list of class names that should be treated as
shared pointers. Classes are resolved through aliases, so any alias
to the defined classes will be considered. Default is
`::std::shared_ptr;::boost::shared_pointer`.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, done. Fixed in the actual checker as well.

#include "../ClangTidyCheck.h"
#include "../utils/Matchers.h"
#include "../utils/OptionsUtils.h"
#include "clang/AST/ASTContext.h"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor

@vbvictor vbvictor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also try to eliminate second hasDescendant(declRefExpr()) in similar way if possible. I'd try to look at it this week too.
I suppose it's the only issue to be resolved before merge.

Copy link
Author

@pizzud pizzud left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll give the matcher simplification a shot and see how it goes; I have a very short working week this week so I'm unlikely to make much progress.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants