Skip to content

[clang] Diagnose [[nodiscard]] return types in Objective-C++ #142541

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 4 commits into
base: main
Choose a base branch
from

Conversation

halbi2
Copy link
Contributor

@halbi2 halbi2 commented Jun 3, 2025

Fixes #141504. @AaronBallman please take a look

My solution was to copy-paste getUnusedResultAttr and hasUnusedResultAttr from CallExpr into ObjCMessageExpr too.

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

llvmbot commented Jun 3, 2025

@llvm/pr-subscribers-clang

Author: None (halbi2)

Changes

Fixes #141504. @aaronballman please take a look

My solution was to copy-paste getUnusedResultAttr and hasUnusedResultAttr from CallExpr into ObjCMessageExpr too.


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

6 Files Affected:

  • (modified) clang/include/clang/AST/ExprObjC.h (+11)
  • (modified) clang/lib/AST/Expr.cpp (+5-6)
  • (modified) clang/lib/AST/ExprObjC.cpp (+21)
  • (modified) clang/lib/Sema/SemaStmt.cpp (+6-7)
  • (modified) clang/test/SemaCXX/warn-unused-result.cpp (+25)
  • (added) clang/test/SemaObjCXX/attr-nodiscard.mm (+26)
diff --git a/clang/include/clang/AST/ExprObjC.h b/clang/include/clang/AST/ExprObjC.h
index f87fa85569c44..a6b2fca3df13a 100644
--- a/clang/include/clang/AST/ExprObjC.h
+++ b/clang/include/clang/AST/ExprObjC.h
@@ -1236,6 +1236,17 @@ class ObjCMessageExpr final
   /// of `instancetype` (in that case it's an expression type).
   QualType getCallReturnType(ASTContext &Ctx) const;
 
+  /// Returns the WarnUnusedResultAttr that is either declared on the called
+  /// method, or its return type declaration, together with a NamedDecl that
+  /// refers to the declaration the attribute is attached onto.
+  std::pair<const NamedDecl *, const Attr *>
+  getUnusedResultAttr(ASTContext &Ctx) const;
+
+  /// Returns true if this message send should warn on unused results.
+  bool hasUnusedResultAttr(ASTContext &Ctx) const {
+    return getUnusedResultAttr(Ctx).second != nullptr;
+  }
+
   /// Source range of the receiver.
   SourceRange getReceiverRange() const;
 
diff --git a/clang/lib/AST/Expr.cpp b/clang/lib/AST/Expr.cpp
index fe874ccd7b60f..1142745a25f1a 100644
--- a/clang/lib/AST/Expr.cpp
+++ b/clang/lib/AST/Expr.cpp
@@ -2869,12 +2869,11 @@ bool Expr::isUnusedResultAWarning(const Expr *&WarnE, SourceLocation &Loc,
       return true;
     }
 
-    if (const ObjCMethodDecl *MD = ME->getMethodDecl())
-      if (MD->hasAttr<WarnUnusedResultAttr>()) {
-        WarnE = this;
-        Loc = getExprLoc();
-        return true;
-      }
+    if (ME->hasUnusedResultAttr(Ctx)) {
+      WarnE = this;
+      Loc = getExprLoc();
+      return true;
+    }
 
     return false;
   }
diff --git a/clang/lib/AST/ExprObjC.cpp b/clang/lib/AST/ExprObjC.cpp
index 79b5db301d414..1de679ab87140 100644
--- a/clang/lib/AST/ExprObjC.cpp
+++ b/clang/lib/AST/ExprObjC.cpp
@@ -12,6 +12,7 @@
 
 #include "clang/AST/ExprObjC.h"
 #include "clang/AST/ASTContext.h"
+#include "clang/AST/Attr.h"
 #include "clang/AST/ComputeDependence.h"
 #include "clang/AST/SelectorLocationsKind.h"
 #include "clang/AST/Type.h"
@@ -272,6 +273,26 @@ QualType ObjCMessageExpr::getCallReturnType(ASTContext &Ctx) const {
   return Ctx.getReferenceQualifiedType(this);
 }
 
+std::pair<const NamedDecl *, const Attr *>
+ObjCMessageExpr::getUnusedResultAttr(ASTContext &Ctx) const {
+  // If the callee is marked nodiscard, return that attribute
+  if (const ObjCMethodDecl *MD = getMethodDecl())
+    if (const auto *A = MD->getAttr<WarnUnusedResultAttr>())
+      return {nullptr, A};
+
+  // If the return type is a struct, union, or enum that is marked nodiscard,
+  // then return the return type attribute.
+  if (const TagDecl *TD = getCallReturnType(Ctx)->getAsTagDecl())
+    if (const auto *A = TD->getAttr<WarnUnusedResultAttr>())
+      return {TD, A};
+
+  for (const auto *TD = getCallReturnType(Ctx)->getAs<TypedefType>(); TD;
+       TD = TD->desugar()->getAs<TypedefType>())
+    if (const auto *A = TD->getDecl()->getAttr<WarnUnusedResultAttr>())
+      return {TD->getDecl(), A};
+  return {nullptr, nullptr};
+}
+
 SourceRange ObjCMessageExpr::getReceiverRange() const {
   switch (getReceiverKind()) {
   case Instance:
diff --git a/clang/lib/Sema/SemaStmt.cpp b/clang/lib/Sema/SemaStmt.cpp
index c943645c3ab9d..91a3b3eb43b1e 100644
--- a/clang/lib/Sema/SemaStmt.cpp
+++ b/clang/lib/Sema/SemaStmt.cpp
@@ -344,13 +344,12 @@ void DiagnoseUnused(Sema &S, const Expr *E, std::optional<unsigned> DiagID) {
       S.Diag(Loc, diag::err_arc_unused_init_message) << R1;
       return;
     }
-    const ObjCMethodDecl *MD = ME->getMethodDecl();
-    if (MD) {
-      if (DiagnoseNoDiscard(S, nullptr, MD->getAttr<WarnUnusedResultAttr>(),
-                            Loc, R1, R2,
-                            /*isCtor=*/false))
-        return;
-    }
+
+    auto [OffendingDecl, A] = ME->getUnusedResultAttr(S.Context);
+    if (DiagnoseNoDiscard(S, OffendingDecl,
+                          cast_or_null<WarnUnusedResultAttr>(A), Loc, R1, R2,
+                          /*isCtor=*/false))
+      return;
   } else if (const PseudoObjectExpr *POE = dyn_cast<PseudoObjectExpr>(E)) {
     const Expr *Source = POE->getSyntacticForm();
     // Handle the actually selected call of an OpenMP specialized call.
diff --git a/clang/test/SemaCXX/warn-unused-result.cpp b/clang/test/SemaCXX/warn-unused-result.cpp
index 5105f347db8b5..a1d201ec861d5 100644
--- a/clang/test/SemaCXX/warn-unused-result.cpp
+++ b/clang/test/SemaCXX/warn-unused-result.cpp
@@ -364,3 +364,28 @@ void id_print_name() {
     ((int(*)())f)();
 }
 } // namespace GH117975
+
+namespace inheritance {
+// Test that [[nodiscard]] is not inherited by derived class types,
+// but is inherited by member functions
+struct [[nodiscard]] E {
+  [[nodiscard]] explicit E(int);
+  explicit E(const char*);
+  [[nodiscard]] int f();
+};
+struct F : E {
+  using E::E;
+};
+E e();
+F f();
+void test() {
+  e();     // expected-warning {{ignoring return value of type 'E' declared with 'nodiscard' attribute}}
+  f();     // no warning: derived class type does not inherit the attribute
+  E(1);    // expected-warning {{ignoring temporary created by a constructor declared with 'nodiscard' attribute}}
+  E("x");  // expected-warning {{ignoring temporary of type 'E' declared with 'nodiscard' attribute}}
+  F(1);    // no warning: inherited constructor does not inherit the attribute either
+  F("x");  // no warning
+  e().f(); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
+  f().f(); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
+}
+} // namespace inheritance
diff --git a/clang/test/SemaObjCXX/attr-nodiscard.mm b/clang/test/SemaObjCXX/attr-nodiscard.mm
new file mode 100644
index 0000000000000..e1eefb74d3961
--- /dev/null
+++ b/clang/test/SemaObjCXX/attr-nodiscard.mm
@@ -0,0 +1,26 @@
+// RUN: %clang_cc1  -fsyntax-only -verify %s
+
+template<class T>
+struct [[nodiscard]] expected {};
+
+using E = expected<int>;
+
+@interface INTF
+- (int) a [[nodiscard]];
++ (int) b [[nodiscard]];
+- (expected<int>) c;
++ (expected<int>) d;
+- (E) e;
++ (E) f;
+- (void) g [[nodiscard]]; // expected-warning {{attribute 'nodiscard' cannot be applied to Objective-C method without return value}}
+@end
+
+void foo(INTF *a) {
+  [a a]; // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
+  [INTF b]; // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
+  [a c]; // expected-warning {{ignoring return value of type 'expected<int>' declared with 'nodiscard' attribute}}
+  [INTF d]; // expected-warning {{ignoring return value of type 'expected<int>' declared with 'nodiscard' attribute}}
+  [a e]; // expected-warning {{ignoring return value of type 'expected<int>' declared with 'nodiscard' attribute}}
+  [INTF f]; // expected-warning {{ignoring return value of type 'expected<int>' declared with 'nodiscard' attribute}}
+  [a g];
+}

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! Can you also add some test coverage for Objective-C (the GNU spelling is always available in C mode and the [[nodiscard]] spelling is available in C23 and later). Also, the changes should come with a release note in clang/docs/ReleaseNotes.rst so users know about the fix.

// If the callee is marked nodiscard, return that attribute
if (const ObjCMethodDecl *MD = getMethodDecl())
if (const auto *A = MD->getAttr<WarnUnusedResultAttr>())
return {nullptr, A};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this be {MD, A}?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is this way in CallExpr::getUnusedResultAttribute too. Probably the NamedDecl is supposed to be set only when it is a type declaration.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you better explain that? What happens if this DOES set the MD correctly? Do we have a test that shows the behavior change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, if this is changed to return {MD, A} then SemaObjC/method-warn-unused-attribute.m fails with

ignoring return value of type 'fee' declared with 'warn_unused_result' attribute

instead of the expected

ignoring return value of function declared with 'warn_unused_result' attribute

Copy link
Member

Choose a reason for hiding this comment

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

For more context, see #112521, but tl;dr all of this is because we want to issue an unused result diagnostic in two different situations: either because the function is declared nodiscard or because the return type is declared nodiscard. If both are nodiscard, then we prefer printing the function as the cause instead of the type.

Copy link
Member

@Sirraide Sirraide Jun 9, 2025

Choose a reason for hiding this comment

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

Specifically,

  • {null, Attribute} means ‘the function is nodiscard’;
  • {TD, Attribute} means ‘the type is nodiscard, please include TD in the diagnostic;
  • {null, null} means ‘neither is nodiscard’.

I thought there was a comment about this somewhere, but if not should probably add one

Copy link
Member

@Sirraide Sirraide Jun 9, 2025

Choose a reason for hiding this comment

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

The attribute I believe is returned so we can get the diagnostic message from it but don’t quote me on that.

@@ -272,6 +273,26 @@ QualType ObjCMessageExpr::getCallReturnType(ASTContext &Ctx) const {
return Ctx.getReferenceQualifiedType(this);
}

std::pair<const NamedDecl *, const Attr *>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since we're casting back and forth always, should this be:

Suggested change
std::pair<const NamedDecl *, const Attr *>
std::pair<const NamedDecl *, const WarnUnusedResultAttr *>

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is this way in CallExpr::getUnusedResultAttr too. I would change it in both places?
Maybe is this way so that different Attrs can be supported in the future, not just WarnUnusedResultAttrs.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we're decades past 'maybe we'll support other kinds some day' :D

If you're willing to do the change on CallExpr at the same time, please do.

// If the callee is marked nodiscard, return that attribute
if (const ObjCMethodDecl *MD = getMethodDecl())
if (const auto *A = MD->getAttr<WarnUnusedResultAttr>())
return {nullptr, A};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why can't this return the MethodDecl here?

Copy link
Contributor

@rjmccall rjmccall left a comment

Choose a reason for hiding this comment

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

Generally LGTM, although I agree with requests above.

Copy link
Member

@Sirraide Sirraide left a comment

Choose a reason for hiding this comment

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

I would prefer if the code from CallExpr::getUnusedResultAttr was factored out into a separate function and reused rather than copy-pasted here since the code is identical save for the class that this is a member of. Specifically, if we make it a function template somewhere (ASTContext?) and pass in the CallExpr/ObjCMessageExpr, we should just be able to reuse it as-is.

@Sirraide
Copy link
Member

Sirraide commented Jun 9, 2025

factored out into a separate function

Not so much factored out as just made into a template since it is already a separate function.

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 objective-c objective-c++
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Should diagnose discard of Objective-C++ method returning nodiscard type
6 participants