-
Notifications
You must be signed in to change notification settings - Fork 13.6k
[clang-tidy] doesNotMutateObject
: Handle calls to member functions …
#94362
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
Changes from 1 commit
8a7e3ee
6c521ec
40b3287
b3d455f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -36,6 +36,111 @@ void extractNodesByIdTo(ArrayRef<BoundNodes> Matches, StringRef ID, | |||||
Nodes.insert(Match.getNodeAs<Node>(ID)); | ||||||
} | ||||||
|
||||||
// If `D` has a const-qualified overload with otherwise identical | ||||||
// ref-qualifiers, returns that overload. | ||||||
const CXXMethodDecl *findConstOverload(const CXXMethodDecl &D) { | ||||||
assert(!D.isConst()); | ||||||
|
||||||
DeclContext::lookup_result lookup_result = | ||||||
D.getParent()->lookup(D.getNameInfo().getName()); | ||||||
if (lookup_result.isSingleResult()) { | ||||||
// No overload. | ||||||
return nullptr; | ||||||
} | ||||||
for (const Decl *overload : lookup_result) { | ||||||
const CXXMethodDecl *candidate = dyn_cast<CXXMethodDecl>(overload); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Type is explicitly stated in same statement. |
||||||
if (candidate && !candidate->isDeleted() && candidate->isConst() && | ||||||
candidate->getRefQualifier() == D.getRefQualifier()) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like we look up by name, do we also need to confirm that the return types are the same, or would overloading otherwise not be allowed and that's why it's implied? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overloading would be allowed in this case, but I think having a That being said, your comment made me realize that we were not checking that the parameter types were the same. Done and added tests ( |
||||||
return candidate; | ||||||
} | ||||||
} | ||||||
return nullptr; | ||||||
} | ||||||
|
||||||
// Returns true if both types refer to the same to the same type, | ||||||
legrosbuffle marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
// ignoring the const-qualifier. | ||||||
bool isSameTypeIgnoringConst(QualType A, QualType B) { | ||||||
A = A.getCanonicalType(); | ||||||
B = B.getCanonicalType(); | ||||||
A.addConst(); | ||||||
B.addConst(); | ||||||
return A == B; | ||||||
} | ||||||
|
||||||
// Returns true if both types are pointers or reference to the same type, | ||||||
// ignoring the const-qualifier. | ||||||
bool pointsToSameTypeIgnoringConst(QualType A, QualType B) { | ||||||
assert(A->isPointerType() || A->isReferenceType()); | ||||||
assert(B->isPointerType() || B->isReferenceType()); | ||||||
return isSameTypeIgnoringConst(A->getPointeeType(), B->getPointeeType()); | ||||||
} | ||||||
|
||||||
// Return true if non-const member function `M` likely does not mutate `*this`. | ||||||
// | ||||||
// Note that if the member call selects a method/operator `f` that | ||||||
// is not const-qualified, then we also consider that the object is | ||||||
// not mutated if: | ||||||
// - (A) there is a const-qualified overload `cf` of `f` that has | ||||||
// the | ||||||
// same ref-qualifiers; | ||||||
// - (B) * `f` returns a value, or | ||||||
// * if `f` returns a `T&`, `cf` returns a `const T&` (up to | ||||||
// possible aliases such as `reference` and | ||||||
// `const_reference`), or | ||||||
// * if `f` returns a `T*`, `cf` returns a `const T*` (up to | ||||||
// possible aliases). | ||||||
// - (C) the result of the call is not mutated. | ||||||
// | ||||||
// The assumption that `cf` has the same semantics as `f`. | ||||||
// For example: | ||||||
// - In `std::vector<T> v; const T t = v[...];`, we consider that | ||||||
// expression `v[...]` does not mutate `v` as | ||||||
// `T& std::vector<T>::operator[]` has a const overload | ||||||
// `const T& std::vector<T>::operator[] const`, and the | ||||||
// result expression of type `T&` is only used as a `const T&`; | ||||||
// - In `std::map<K, V> m; V v = m.at(...);`, we consider | ||||||
// `m.at(...)` to be an immutable access for the same reason. | ||||||
// However: | ||||||
// - In `std::map<K, V> m; const V v = m[...];`, We consider that | ||||||
// `m[...]` mutates `m` as `V& std::map<K, V>::operator[]` does | ||||||
// not have a const overload. | ||||||
// - In `std::vector<T> v; T& t = v[...];`, we consider that | ||||||
// expression `v[...]` mutates `v` as the result is kept as a | ||||||
// mutable reference. | ||||||
// | ||||||
// This function checks (A) ad (B), but the caller should make sure that the | ||||||
// object is not mutated through the return value. | ||||||
bool isLikelyShallowConst(const CXXMethodDecl &M) { | ||||||
assert(!M.isConst()); | ||||||
// The method can mutate our variable. | ||||||
|
||||||
// (A) | ||||||
const CXXMethodDecl *ConstOverload = findConstOverload(M); | ||||||
if (ConstOverload == nullptr) { | ||||||
return false; | ||||||
} | ||||||
|
||||||
// (B) | ||||||
const QualType CallTy = M.getReturnType().getCanonicalType(); | ||||||
const QualType OverloadTy = ConstOverload->getReturnType().getCanonicalType(); | ||||||
if (CallTy->isReferenceType()) { | ||||||
if (!(OverloadTy->isReferenceType() && | ||||||
legrosbuffle marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
pointsToSameTypeIgnoringConst(CallTy, OverloadTy))) { | ||||||
return false; | ||||||
} | ||||||
legrosbuffle marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} else if (CallTy->isPointerType()) { | ||||||
if (!(OverloadTy->isPointerType() && | ||||||
pointsToSameTypeIgnoringConst(CallTy, OverloadTy))) { | ||||||
return false; | ||||||
} | ||||||
} else { | ||||||
if (!isSameTypeIgnoringConst(CallTy, OverloadTy)) { | ||||||
return false; | ||||||
} | ||||||
} | ||||||
return true; | ||||||
} | ||||||
|
||||||
// A matcher that matches DeclRefExprs that are used in ways such that the | ||||||
// underlying declaration is not modified. | ||||||
// If the declaration is of pointer type, `Indirections` specifies the level | ||||||
|
@@ -54,16 +159,15 @@ void extractNodesByIdTo(ArrayRef<BoundNodes> Matches, StringRef ID, | |||||
// matches (A). | ||||||
// | ||||||
AST_MATCHER_P(DeclRefExpr, doesNotMutateObject, int, Indirections) { | ||||||
// We walk up the parents of the DeclRefExpr recursively until we end up on a | ||||||
// parent that cannot modify the underlying object. There are a few kinds of | ||||||
// expressions: | ||||||
// - Those that cannot be used to mutate the underlying object. We can stop | ||||||
// We walk up the parents of the DeclRefExpr recursively. There are a few | ||||||
// kinds of expressions: | ||||||
// - Those that cannot be used to mutate the underlying variable. We can stop | ||||||
// recursion there. | ||||||
// - Those that can be used to mutate the underlying object in analyzable | ||||||
// - Those that can be used to mutate the underlying variable in analyzable | ||||||
// ways (such as taking the address or accessing a subobject). We have to | ||||||
// examine the parents. | ||||||
// - Those that we don't know how to analyze. In that case we stop there and | ||||||
// we assume that they can mutate the underlying expression. | ||||||
// we assume that they can modify the expression. | ||||||
|
||||||
struct StackEntry { | ||||||
StackEntry(const Expr *E, int Indirections) | ||||||
|
@@ -90,7 +194,7 @@ AST_MATCHER_P(DeclRefExpr, doesNotMutateObject, int, Indirections) { | |||||
assert(Ty->isPointerType()); | ||||||
Ty = Ty->getPointeeType().getCanonicalType(); | ||||||
} | ||||||
if (Ty.isConstQualified()) | ||||||
if (Ty->isVoidType() || Ty.isConstQualified()) | ||||||
continue; | ||||||
|
||||||
// Otherwise we have to look at the parents to see how the expression is | ||||||
|
@@ -159,11 +263,56 @@ AST_MATCHER_P(DeclRefExpr, doesNotMutateObject, int, Indirections) { | |||||
// The method call cannot mutate our variable. | ||||||
continue; | ||||||
} | ||||||
if (isLikelyShallowConst(*Method)) { | ||||||
// We still have to check that the object is not modified through | ||||||
// the method's return value (C). | ||||||
const auto MemberParents = Ctx.getParents(*Member); | ||||||
assert(MemberParents.size() == 1); | ||||||
const CallExpr *Call = MemberParents[0].get<CallExpr>(); | ||||||
legrosbuffle marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
// If `o` is an object of class type and `f` is a member function, | ||||||
// then `o.f` has to be used as part of a call expression. | ||||||
assert(Call != nullptr && "member function has to be called"); | ||||||
Stack.emplace_back( | ||||||
Call, | ||||||
Method->getReturnType().getCanonicalType()->isPointerType() | ||||||
? 1 | ||||||
: 0); | ||||||
continue; | ||||||
} | ||||||
return false; | ||||||
} | ||||||
Stack.emplace_back(Member, 0); | ||||||
continue; | ||||||
} | ||||||
if (const auto *const OpCall = dyn_cast<CXXOperatorCallExpr>(P)) { | ||||||
// Operator calls have function call syntax. The `*this` parameter | ||||||
// is the first parameter. | ||||||
if (OpCall->getNumArgs() == 0 || OpCall->getArg(0) != Entry.E) { | ||||||
return false; | ||||||
} | ||||||
const auto *const Method = | ||||||
dyn_cast<CXXMethodDecl>(OpCall->getDirectCallee()); | ||||||
|
||||||
if (Method == nullptr) { | ||||||
// This is not a member operator. Typically, a friend operator. These | ||||||
// are handled like function calls. | ||||||
return false; | ||||||
} | ||||||
|
||||||
if (Method->isConst() || Method->isStatic()) { | ||||||
continue; | ||||||
} | ||||||
if (isLikelyShallowConst(*Method)) { | ||||||
// We still have to check that the object is not modified through | ||||||
// the operator's return value (C). | ||||||
Stack.emplace_back( | ||||||
OpCall, | ||||||
Method->getReturnType().getCanonicalType()->isPointerType() ? 1 | ||||||
: 0); | ||||||
continue; | ||||||
} | ||||||
return false; | ||||||
} | ||||||
|
||||||
if (const auto *const Op = dyn_cast<UnaryOperator>(P)) { | ||||||
switch (Op->getOpcode()) { | ||||||
|
Uh oh!
There was an error while loading. Please reload this page.