This is an archive of the discontinued LLVM Phabricator instance.

[clang-tidy] Use-after-move: Ignore moves inside a try_emplace.
ClosedPublic

Authored by mboehme on Mar 5 2021, 4:28 AM.

Details

Summary

We have no way to reason about the bool returned by try_emplace, so we
simply ignore any std::move()s that happen in a try_emplace argument.
A lot of the time in this situation, the code will be checking the
bool and doing something else if it turns out the value wasn't moved
into the map, and this has been causing false positives so far.

I don't currently have any intentions of handling "maybe move" functions
more generally.

Diff Detail

Event Timeline

mboehme created this revision.Mar 5 2021, 4:28 AM
mboehme requested review of this revision.Mar 5 2021, 4:28 AM
Herald added a project: Restricted Project. · View Herald TranscriptMar 5 2021, 4:28 AM
Herald added a subscriber: cfe-commits. · View Herald Transcript
sammccall accepted this revision.Mar 5 2021, 4:44 AM

Nice! Only substantive suggestion: I think requiring the base type to be exactly a standard map type is too conservative.

clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp
396

I'd suggest dropping this condition.

Custom containers often emulate the standard library (e.g. llvm::DenseMap implements try_emplace, as do the absl maps and I found several other examples).

It seems very unlikely to encounter a try_emplace function without these semantics.
(The only case I can imagine is if for some reason they didn't bother to implement the bool return value, so there was no way to know whether emplacement happened. False negative there doesn't seem bad).

402

Maybe explain why try_emplace is special?

try_emplace is a common maybe-moving function that returns a bool that tells callers whether it moved.
Ignore std::move inside try_emplace to avoid false positives, as we don't track uses of the bool.

405

i'm trying to think if there's an important case where the call isn't the immediate parent of the move and yet we're still maybe-moving... but I can't think of one. (Just mentioning in case one occurs to you)

clang-tools-extra/docs/clang-tidy/checks/bugprone-use-after-move.rst
173

s/ignored/conservatively assumed *not* to move/?

(It wasn't obvious to me when reading how "ignored" in the implementation translates to the domain)

clang-tools-extra/test/clang-tidy/checkers/bugprone-use-after-move.cpp
510

calling this function exactly try_emplace seems gratuitiously confusing - since the condition we're changing here is "calls inside a function named try_emplace", just with a different definition of "inside"...

818

hahaha :-)

This revision is now accepted and ready to land.Mar 5 2021, 4:44 AM
mboehme updated this revision to Diff 328491.Mar 5 2021, 5:49 AM

Responses to review comments.

mboehme marked 5 inline comments as done.Mar 5 2021, 5:52 AM
mboehme added inline comments.
clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp
396

Thanks -- I wasn't sure myself whether I should be so restrictive, and you make some very convincing points. Done!

402

I've stolen your wording -- thanks!

405

Yeah, I thought about that myself but couldn't come up with anything.

clang-tools-extra/test/clang-tidy/checkers/bugprone-use-after-move.cpp
818

Yeah, I was being a bit sloppy with my definition of what a map is...

njames93 requested changes to this revision.EditedMar 5 2021, 6:04 AM
njames93 added a subscriber: njames93.

While try_emplace is a special case, it's may not be the only special case in someone's codebase, this should be extended with options to let users handle their special containers.

clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp
406

Going with a custom list of bad names I'd suggest matching on function decls instead of method decls.

This revision now requires changes to proceed.Mar 5 2021, 6:04 AM
This comment was removed by njames93.
This revision was not accepted when it landed; it landed in state Needs Revision.Mar 5 2021, 6:06 AM
This revision was landed with ongoing or failed builds.
This revision was automatically updated to reflect the committed changes.
mboehme marked 3 inline comments as done.

While try_emplace is a special case, it's may not be the only special case in someone's codebase, this should be extended with options to let users handle their special containers.

Sorry, I didn't see your comment before submitting.

I agree that there may be other cases of "maybe move" functions, but try_emplace seems to be the most common case. Handling "maybe move" functions more general would require some way of annotating them; I felt it was useful to get the try_emplace case handled first.

While try_emplace is a special case, it's may not be the only special case in someone's codebase, this should be extended with options to let users handle their special containers.

Sorry, I didn't see your comment before submitting.

I agree that there may be other cases of "maybe move" functions, but try_emplace seems to be the most common case. Handling "maybe move" functions more general would require some way of annotating them; I felt it was useful to get the try_emplace case handled first.

The canonical way of doing this in clang-tidy is to add an option to the check which takes a semi-colon delimited list.
clang::tidy::utils::options::parseStringList can then turn that into a vector of string.
Annoyingly the hasAnyName matcher needs a vector of StringRef's so most checks do something like this

hasAnyName(SmallVector<StringRef, 4>(
      StringLikeClasses.begin(), StringLikeClasses.end())

As you are reading options, you will need to override the storeOptions method in the check to then store the value read from config, serializeStringList is your friend here.
The documentation will also need updating to describe the option and what it does.

First of all, sorry for submitting this change prematurely. I don't contribute to LLVM/Clang regularly and forgot that reviews should be kept open for a while after a reviewer has approved them to give others a chance to chime in. I jumped the gun here -- apologies.

While try_emplace is a special case, it's may not be the only special case in someone's codebase, this should be extended with options to let users handle their special containers.

Sorry, I didn't see your comment before submitting.

I agree that there may be other cases of "maybe move" functions, but try_emplace seems to be the most common case. Handling "maybe move" functions more general would require some way of annotating them; I felt it was useful to get the try_emplace case handled first.

The canonical way of doing this in clang-tidy is to add an option to the check which takes a semi-colon delimited list.
clang::tidy::utils::options::parseStringList can then turn that into a vector of string.
Annoyingly the hasAnyName matcher needs a vector of StringRef's so most checks do something like this

hasAnyName(SmallVector<StringRef, 4>(
      StringLikeClasses.begin(), StringLikeClasses.end())

As you are reading options, you will need to override the storeOptions method in the check to then store the value read from config, serializeStringList is your friend here.
The documentation will also need updating to describe the option and what it does.

I think I shouldn't just use a functionDecl(hasAnyName()) matcher with a string list; here's why.

In the case of try_emplace, it makes sense to match on any method (or even a free function) with that name. I was originally restricting myself to only the standard map types, but as @sammccall pointed out to me, someone using this pretty unique name in their own code is very likely to be using the same semantics as the standard map types.

This may not be true for all methods that a user might want to specify though. Say a user has

class MyClass {
public:
  bool Insert(OtherClass &&);
};

where, as for try_emplace, the boolean return value says whether the insertion actually happened.

We wouldn't simply want to ignore std::move arguments on any method called Insert. It's likely that there are other methods called Insert in the codebase that take an rvalue reference and move from it unconditionally, and we want the check to look at those.

So we'd want the user to be able to specify the qualified name MyClass::Insert as the function to ignore. This makes the matcher a bit more interesting as we'd need to work out when seeing a string like this whether MyClass is a class name or a namespace name. Or we might simply do something like

anyOf(
    // maybe `MyClass` is a namespace...
    functionDecl(hasName("MyClass::Insert")),
    // ...or maybe it's a class
    cxxMethodDecl(ofClass(hasName("MyClass")), hasName("Insert"))
)

I assume there isn't an existing matcher that does this -- or are you aware of one?