Skip to content

[Clang][Sema]: Diagnose lambda to bool implicit casts #83152

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

Merged

Conversation

vinayakdsci
Copy link
Contributor

Fixes #82512

Adds diagnostics for lambda expressions being cast to boolean values, which results in the expression always evaluating to true.
Earlier, Clang allowed compilation of such erroneous programs, but now emits a warning through -Wpointer-bool-conversion.

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Feb 27, 2024
@llvmbot
Copy link
Member

llvmbot commented Feb 27, 2024

@llvm/pr-subscribers-clang

Author: Vinayak Dev (vinayakdsci)

Changes

Fixes #82512

Adds diagnostics for lambda expressions being cast to boolean values, which results in the expression always evaluating to true.
Earlier, Clang allowed compilation of such erroneous programs, but now emits a warning through -Wpointer-bool-conversion.


Full diff: https://github.com/llvm/llvm-project/pull/83152.diff

1 Files Affected:

  • (modified) clang/lib/Sema/SemaChecking.cpp (+21)
diff --git a/clang/lib/Sema/SemaChecking.cpp b/clang/lib/Sema/SemaChecking.cpp
index 0de76ee119cf81..3b03036587baeb 100644
--- a/clang/lib/Sema/SemaChecking.cpp
+++ b/clang/lib/Sema/SemaChecking.cpp
@@ -16538,6 +16538,27 @@ void Sema::DiagnoseAlwaysNonNullPointer(Expr *E,
     }
   }
 
+  // Complain if we are converting a lambda expression to a boolean value
+  if (auto *MCallExpr = dyn_cast<CXXMemberCallExpr>(E)) {
+    if (MCallExpr->getObjectType()->isRecordType()) {
+      if (auto *MRecordDecl = MCallExpr->getRecordDecl()) {
+        if (MRecordDecl->isLambda()) {
+          std::string Str;
+          llvm::raw_string_ostream S(Str);
+          const unsigned DiagID = diag::warn_impcast_pointer_to_bool;
+          // For lambdas, the pointer type is function, which corresponds to 1.
+          const unsigned FunctionPointerType = 1;
+          // Pretty print the diagnostic for the warning
+          E->printPretty(S, nullptr, getPrintingPolicy());
+          Diag(E->getExprLoc(), DiagID)
+              << FunctionPointerType << S.str() << E->getSourceRange() << Range
+              << IsEqual;
+          return;
+        }
+      }
+    }
+  }
+
   // Expect to find a single Decl.  Skip anything more complicated.
   ValueDecl *D = nullptr;
   if (DeclRefExpr *R = dyn_cast<DeclRefExpr>(E)) {

Copy link
Collaborator

@AaronBallman AaronBallman left a comment

Choose a reason for hiding this comment

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

Thank you for the fix!

There are some related test failures caught by precommit CI that need to be addressed.

The issue in dr18xx.cpp looks to be a case where we should replace bool b = with auto b = (it doesn't change the testing for what's discussed in http://wg21.link/cwg1837).

The issues in blocks.mm should get new expected warning markings for the improved diagnostics.

You should also add a release note to clang/docs/ReleaseNotes.rst so users know about the fix (and mention the issue number as well).

The changes should come with new test coverage, probably in one of the existing tests in clang/test/SemaCXX/.

Comment on lines 16542 to 16544
if (auto *MCallExpr = dyn_cast<CXXMemberCallExpr>(E)) {
if (MCallExpr->getObjectType()->isRecordType()) {
if (auto *MRecordDecl = MCallExpr->getRecordDecl()) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
if (auto *MCallExpr = dyn_cast<CXXMemberCallExpr>(E)) {
if (MCallExpr->getObjectType()->isRecordType()) {
if (auto *MRecordDecl = MCallExpr->getRecordDecl()) {
if (const auto *MCallExpr = dyn_cast<CXXMemberCallExpr>(E)) {
if (MCallExpr->getObjectType()->isRecordType()) {
if (const auto *MRecordDecl = MCallExpr->getRecordDecl()) {

Aiming for better const correctness, but if this doesn't work, don't worry about making the changes.

Comment on lines 16546 to 16553
std::string Str;
llvm::raw_string_ostream S(Str);
const unsigned DiagID = diag::warn_impcast_pointer_to_bool;
// For lambdas, the pointer type is function, which corresponds to 1.
const unsigned FunctionPointerType = 1;
// Pretty print the diagnostic for the warning
E->printPretty(S, nullptr, getPrintingPolicy());
Diag(E->getExprLoc(), DiagID)
<< FunctionPointerType << S.str() << E->getSourceRange() << Range
<< IsEqual;
return;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
std::string Str;
llvm::raw_string_ostream S(Str);
const unsigned DiagID = diag::warn_impcast_pointer_to_bool;
// For lambdas, the pointer type is function, which corresponds to 1.
const unsigned FunctionPointerType = 1;
// Pretty print the diagnostic for the warning
E->printPretty(S, nullptr, getPrintingPolicy());
Diag(E->getExprLoc(), DiagID)
<< FunctionPointerType << S.str() << E->getSourceRange() << Range
<< IsEqual;
return;
Diag(E->getExprLoc(), diag::warn_impcast_pointer_to_bool)
<< /*FunctionPointerType*/ 1 << S.str() << E->getSourceRange() << Range
<< IsEqual;
return;

Two changes happening here: 1) moves some constant variables inline, 2) removes the call to printPretty().

(2) is because we expect diagnostic output to all be piped through the diagnostics engine, so that we can do things like format it differently (e.g., perhaps print it as SARIF rather than text)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, understood. I will make the required changes, and force push again. Thanks for the review!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removing the call to printPretty will not populate S, so the diagnostic will not have the name for the identifier describing the error-causing function but instead display ' '. Is there a workaround for this? Or should I leave it be? Thanks!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh gosh, this was a think-o on my part, you're right, that is being passed through the diagnostics engine. Sorry for the noise, I must have been tired. :-)

@vinayakdsci
Copy link
Contributor Author

There are some related test failures caught by precommit CI that need to be addressed.

The issue in dr18xx.cpp looks to be a case where we should replace bool b = with auto b = (it doesn't change the testing for what's discussed in http://wg21.link/cwg1837).

Changing bool b = to auto b = does not compile, as the compiler complains that auto is not allowed in non-static struct members. b cannot be declared as static either, which would cause an invalid use of this outside of a non-static member function. It looks like type deduction using auto for member variables is not allowed.

@AaronBallman
Copy link
Collaborator

There are some related test failures caught by precommit CI that need to be addressed.
The issue in dr18xx.cpp looks to be a case where we should replace bool b = with auto b = (it doesn't change the testing for what's discussed in http://wg21.link/cwg1837).

Changing bool b = to auto b = does not compile, as the compiler complains that auto is not allowed in non-static struct members. b cannot be declared as static either, which would cause an invalid use of this outside of a non-static member function. It looks like type deduction using auto for member variables is not allowed.

Yeah, another think-o on my part. I think it's fine to add the expected diagnostic to the file instead of silencing the warning.

@vinayakdsci vinayakdsci force-pushed the pointer-bool-address-warning-fix branch from 859ce0b to 0e9d7e7 Compare February 28, 2024 14:26
@vinayakdsci vinayakdsci requested a review from Endilll as a code owner February 28, 2024 14:26
@vinayakdsci vinayakdsci force-pushed the pointer-bool-address-warning-fix branch from 0e9d7e7 to c65c6a3 Compare February 28, 2024 14:27
@@ -287,6 +287,11 @@ namespace dr1837 { // dr1837: 3.3
};
};
};
/* since-cxx11-warning@-6{{address of function '[] {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you convert this to use a marker? We don't want readers to count relative offsets. In other words, it should read since-cxx11-warning@#dr1837-lambda {{address of function ...

You can see an example in the test for 1890.

Comment on lines 16542 to 16556
if (const auto *MCallExpr = dyn_cast<CXXMemberCallExpr>(E)) {
if (MCallExpr->getObjectType()->isRecordType()) {
if (const auto *MRecordDecl = MCallExpr->getRecordDecl()) {
if (MRecordDecl->isLambda()) {
std::string Str;
llvm::raw_string_ostream S(Str);

E->printPretty(S, nullptr, getPrintingPolicy());
Diag(E->getExprLoc(), diag::warn_impcast_pointer_to_bool)
<< /*FunctionPointerType*/ 1 << S.str() << E->getSourceRange()
<< Range << IsEqual;
return;
}
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you actually need the call to isRecordType()? I would have thought this was equivalent.

Suggested change
if (const auto *MCallExpr = dyn_cast<CXXMemberCallExpr>(E)) {
if (MCallExpr->getObjectType()->isRecordType()) {
if (const auto *MRecordDecl = MCallExpr->getRecordDecl()) {
if (MRecordDecl->isLambda()) {
std::string Str;
llvm::raw_string_ostream S(Str);
E->printPretty(S, nullptr, getPrintingPolicy());
Diag(E->getExprLoc(), diag::warn_impcast_pointer_to_bool)
<< /*FunctionPointerType*/ 1 << S.str() << E->getSourceRange()
<< Range << IsEqual;
return;
}
}
}
}
if (const auto *MCallExpr = dyn_cast<CXXMemberCallExpr>(E)) {
if (const auto *MRecordDecl = MCallExpr->getRecordDecl(); MRecordDecl && MRecordDecl->isLambda()) {
std::string Str;
llvm::raw_string_ostream S(Str);
E->printPretty(S, nullptr, getPrintingPolicy());
Diag(E->getExprLoc(), diag::warn_impcast_pointer_to_bool)
<< /*FunctionPointerType*/ 1 << S.str() << E->getSourceRange()
<< Range << IsEqual;
return;
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually, I'm not convinced we should print the entire expression in this case. Lambdas usually aren't nearly as trivial as the ones we have in our test cases, and can be arbitrarily long. e.g.,

C:\Users\aballman\OneDrive - Intel Corporation\Desktop\test.cpp:2:7: warning: address of function '[]() {
    int foo = 0;
    return foo;
}' will always evaluate to 'true' [-Wpointer-bool-conversion]
    2 |   if ([](){
      |   ~~  ^~~~~
    3 |     // This is a comment
      |     ~~~~~~~~~~~~~~~~~~~~
    4 |     // intended to make the lambda
      |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    5 |     // longer so that it shows
      |     ~~~~~~~~~~~~~~~~~~~~~~~~~~
    6 |     // what the diagnostic behavior is
      |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    7 |     int foo = 0;
      |     ~~~~~~~~~~~~
    8 |     // of really large lambdas, and whether
      |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    9 |     // it is worth it to print out the entire
      |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   10 |     // expression in this case.
      |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~
   11 |     return foo;
      |     ~~~~~~~~~~~
   12 |   }) {
      |   ~
1 warning generated.

This has two problems: 1) it repeats the user's code in both the diagnostic and in the highlighting, 2) the amount of context is huge and distracts from the diagnostic.

I think we should modify warn_impcast_pointer_to_bool to something along these lines:

def warn_impcast_pointer_to_bool : Warning<
    "address of%select{| function| array| lambda function pointer conversion operator}0 '%1' will always evaluate to 'true'">, InGroup<PointerBoolConversion>;

(Will need to be reformatted for the usual 80-col limits.) And when passing the source range for the lambda expression, I think we should pass only the lambda introducer as the source range so that we highlight far less code.

WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is actually a great idea! and then the diagnostic would simply say address of lambda function {..} will always evaluate to true. Would actually be more informative than printing the entire expression in my opinion. Should I modify the code to do this instead??

Copy link
Collaborator

Choose a reason for hiding this comment

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

Should I modify the code to do this instead??

Yes please!

@vinayakdsci vinayakdsci force-pushed the pointer-bool-address-warning-fix branch from c65c6a3 to 613e7c0 Compare February 28, 2024 15:45
@vinayakdsci
Copy link
Contributor Author

Done! I added the diagnostic to the TableGen file, and passed in only the Record's source range instead of the whole expression's. Now only the lambda's initializer is highlighted in the diagnostic. I hope this does it!

Comment on lines 4130 to 4131
"address of%select{| function| array| lambda function pointer conversion operator}0 '%1' "
"will always evaluate to 'true'">,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
"address of%select{| function| array| lambda function pointer conversion operator}0 '%1' "
"will always evaluate to 'true'">,
"address of %select{'%1'|function '%1'|array '%1'|lambda function pointer "
"conversion operator}0 will always evaluate to 'true'">,

Wrapping to 80-col limits, moves %1 so it's not used for lambda conversion operators.

Comment on lines 16545 to 16553
std::string Str;
llvm::raw_string_ostream S(Str);

E->printPretty(S, nullptr, getPrintingPolicy());
Diag(E->getExprLoc(), diag::warn_impcast_pointer_to_bool)
<< /*LambdaPointerConversionOperatorType*/ 3 << S.str()
<< MRecordDecl->getSourceRange() << Range << IsEqual;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
std::string Str;
llvm::raw_string_ostream S(Str);
E->printPretty(S, nullptr, getPrintingPolicy());
Diag(E->getExprLoc(), diag::warn_impcast_pointer_to_bool)
<< /*LambdaPointerConversionOperatorType*/ 3 << S.str()
<< MRecordDecl->getSourceRange() << Range << IsEqual;
Diag(E->getExprLoc(), diag::warn_impcast_pointer_to_bool)
<< /*LambdaPointerConversionOperatorType*/ 3
<< MRecordDecl->getSourceRange() << Range << IsEqual;

This way we don't pretty print the entire lambda body in the diagnostic message.

@vinayakdsci vinayakdsci force-pushed the pointer-bool-address-warning-fix branch from 613e7c0 to 640cbf9 Compare February 28, 2024 17:38
@vinayakdsci
Copy link
Contributor Author

@AaronBallman sorry for repeating the same mistake twice, I was unable to correctly interpret what you wanted to say when you meant removing the lambda body from the diagnostic.
I have made all the suggested changes and updated the tests. I hope I don't come across as dumb :-) ! I will be more mindful of what changes I have made before pushing them to the remote in the future.

Thanks!

@AaronBallman
Copy link
Collaborator

@AaronBallman sorry for repeating the same mistake twice, I was unable to correctly interpret what you wanted to say when you meant removing the lambda body from the diagnostic. I have made all the suggested changes and updated the tests. I hope I don't come across as dumb :-) ! I will be more mindful of what changes I have made before pushing them to the remote in the future.

No worries at all, your contribution is very appreciated! Also, it's not like I have room to judge -- I suggested breaking code and making a useless test! We all make mistakes. ;-)

Copy link
Collaborator

@AaronBallman AaronBallman left a comment

Choose a reason for hiding this comment

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

LGTM! @Endilll, do you have any concerns?

Copy link
Contributor

@Endilll Endilll left a comment

Choose a reason for hiding this comment

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

DR test changes look good, save for a nit.

@@ -281,7 +281,7 @@ namespace dr1837 { // dr1837: 3.3

struct A {
int f();
bool b = [] {
bool b = [] { // since-cxx11-warning{{address of lambda function pointer conversion operator will always evaluate to 'true'}}
Copy link
Contributor

Choose a reason for hiding this comment

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

Convert this to // since-cxx11-warning@-1 style, to follow the rest of the file.

Copy link
Collaborator

@shafik shafik left a comment

Choose a reason for hiding this comment

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

Thank you for this fix. I am happy the changes were not too different from my suggestion. Sometimes problems end up being more difficult then they seem initially.

if (const auto *MRecordDecl = MCallExpr->getRecordDecl();
MRecordDecl && MRecordDecl->isLambda()) {
Diag(E->getExprLoc(), diag::warn_impcast_pointer_to_bool)
<< /*LambdaPointerConversionOperatorType*/ 3
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
<< /*LambdaPointerConversionOperatorType*/ 3
<< /*LambdaPointerConversionOperatorType=*/ 3

Copy link
Collaborator

Choose a reason for hiding this comment

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

nitpick just to be consistent w/ bugprone-argument-comment, although I don't think it actually catches these cases.

@vinayakdsci vinayakdsci force-pushed the pointer-bool-address-warning-fix branch from 640cbf9 to 60620d1 Compare February 29, 2024 05:03
Copy link

github-actions bot commented Feb 29, 2024

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

@vinayakdsci vinayakdsci force-pushed the pointer-bool-address-warning-fix branch from 60620d1 to 3d6100a Compare February 29, 2024 05:08
@vinayakdsci
Copy link
Contributor Author

@shafik I have made the change in the code as you suggested. If everything seems alright, could you land this for me?

Thanks a lot!

@MaskRay
Copy link
Member

MaskRay commented Mar 1, 2024

Created #83497 as a follow-up to suppress the diagnostic for certain template instantiation uses.

I made one change to unblock our internal users

#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpointer-bool-conversion"
#endif
    // functor_ may be a lambda, which is convertible to bool, leading to a
    // -Wpointer-bool-conversion warning because the value is always true.
    return functor_ == nullptr;
#ifdef __clang__
#pragma clang diagnostic pop
#endif

and then (when I saw another instance of wrapping a callable object) realized we probably should suppress the diagnostic for instantiation. This is similar to how we suppress the diagnostic for instantiations for -Wtautological-compare

MaskRay added a commit that referenced this pull request Mar 1, 2024
…nversion diagnostic during instantiation (#83497)

I have seen two internal pieces of code that uses a template type
parameter to accept any callable type (function pointer, std::function,
closure type, etc). The diagnostic added in #83152 would require
adaptation to the template, which is difficult and also seems
unnecessary. Example:

```cpp
template <typename... Ts>
static bool IsFalse(const Ts&...) { return false; }

template <typename T, typename... Ts,
          typename = typename std::enable_if<std::is_constructible<bool, const T&>::value>::type>
static bool IsFalse(const T& p, const Ts&...) {
  return p ? false : true;
}

template <typename... Args>
void Init(Args&&... args) {
  if (IsFalse(absl::implicit_cast<const typename std::decay<Args>::type&>(
              args)...)) {
    // A callable object convertible to false is either a null pointer or a
    // null functor (e.g., a default-constructed std::function).
    empty_ = true;
  } else {
    empty_ = false;
    new (&factory_) Factory(std::forward<Args>(args)...);
  }
}
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

-Wpointer-bool-conversion/-Waddress misses an obvious case
6 participants