Skip to content

[clang] Generate note on declaration for nodiscard-related attributes #112289

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

Closed
wants to merge 1 commit into from

Conversation

Mick235711
Copy link
Contributor

Currently, the following program

[[nodiscard]] int fun() { return 1; }
[[deprecated]] int fun2() { return 2; }

int main()
{
    fun(); fun2();
}

generates the following diagnostics on Clang trunk: (Compiler Explorer)

<source>:6:5: warning: ignoring return value of function declared with 'nodiscard' attribute [-Wunused-result]
    6 |     fun(); fun2();
      |     ^~~
<source>:6:12: warning: 'fun2' is deprecated [-Wdeprecated-declarations]
    6 |     fun(); fun2();
      |            ^
<source>:2:3: note: 'fun2' has been explicitly marked deprecated here
    2 | [[deprecated]] int fun2() { return 2; }
      |   ^
2 warnings generated.

There seems to exist a discrepancy between [[deprecated]] and [[nodiscard]]. The former generates a warning on the usage of the function, together with a note on the declaration of the function. In contrast, the latter only generates a warning.

This PR tries to fix this discrepancy by additionally generating a note on the declaration of all [[nodiscard]]-related attributes (nodiscard, warn_unused_result, pure, and const). All 4 attributes generate a warning on ignoring the return value/constructor result on the trunk, and after this PR, all 4 attributes additionally generate a note. The above program will output the following diagnostics after this PR:

test.cpp:6:5: warning: ignoring return value of function declared with 'nodiscard' attribute [-Wunused-result]
    6 |     fun(); fun2();
      |     ^~~
test.cpp:1:3: note: 'fun' has been explicitly marked nodiscard here
    1 | [[nodiscard]] int fun() { return 1; }
      |   ^
test.cpp:6:12: warning: 'fun2' is deprecated [-Wdeprecated-declarations]
    6 |     fun(); fun2();
      |            ^
test.cpp:2:3: note: 'fun2' has been explicitly marked deprecated here
    2 | [[deprecated]] int fun2() { return 2; }
      |   ^
2 warnings generated.

FIrst time contributor here who is not very familiar with Clang's infrastructure... Any comments and suggestions are welcome. No new test cases are added, as I felt that adding expected-notes on existing test cases already serves as testing for this PR.

Copy link

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:openmp OpenMP related changes to Clang labels Oct 15, 2024
@llvmbot
Copy link
Member

llvmbot commented Oct 15, 2024

@llvm/pr-subscribers-clang

Author: Yihe Li (Mick235711)

Changes

Currently, the following program

[[nodiscard]] int fun() { return 1; }
[[deprecated]] int fun2() { return 2; }

int main()
{
    fun(); fun2();
}

generates the following diagnostics on Clang trunk: (Compiler Explorer)

&lt;source&gt;:6:5: warning: ignoring return value of function declared with 'nodiscard' attribute [-Wunused-result]
    6 |     fun(); fun2();
      |     ^~~
&lt;source&gt;:6:12: warning: 'fun2' is deprecated [-Wdeprecated-declarations]
    6 |     fun(); fun2();
      |            ^
&lt;source&gt;:2:3: note: 'fun2' has been explicitly marked deprecated here
    2 | [[deprecated]] int fun2() { return 2; }
      |   ^
2 warnings generated.

There seems to exist a discrepancy between [[deprecated]] and [[nodiscard]]. The former generates a warning on the usage of the function, together with a note on the declaration of the function. In contrast, the latter only generates a warning.

This PR tries to fix this discrepancy by additionally generating a note on the declaration of all [[nodiscard]]-related attributes (nodiscard, warn_unused_result, pure, and const). All 4 attributes generate a warning on ignoring the return value/constructor result on the trunk, and after this PR, all 4 attributes additionally generate a note. The above program will output the following diagnostics after this PR:

test.cpp:6:5: warning: ignoring return value of function declared with 'nodiscard' attribute [-Wunused-result]
    6 |     fun(); fun2();
      |     ^~~
test.cpp:1:3: note: 'fun' has been explicitly marked nodiscard here
    1 | [[nodiscard]] int fun() { return 1; }
      |   ^
test.cpp:6:12: warning: 'fun2' is deprecated [-Wdeprecated-declarations]
    6 |     fun(); fun2();
      |            ^
test.cpp:2:3: note: 'fun2' has been explicitly marked deprecated here
    2 | [[deprecated]] int fun2() { return 2; }
      |   ^
2 warnings generated.

FIrst time contributor here who is not very familiar with Clang's infrastructure... Any comments and suggestions are welcome. No new test cases are added, as I felt that adding expected-notes on existing test cases already serves as testing for this PR.


Patch is 23.10 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/112289.diff

12 Files Affected:

  • (modified) clang/include/clang/AST/Expr.h (+3-2)
  • (modified) clang/include/clang/Basic/DiagnosticSemaKinds.td (+2)
  • (modified) clang/lib/AST/Expr.cpp (+6-4)
  • (modified) clang/lib/Sema/SemaStmt.cpp (+42-21)
  • (modified) clang/test/CXX/dcl.dcl/dcl.attr/dcl.attr.nodiscard/p2.cpp (+15-14)
  • (modified) clang/test/CXX/dcl.dcl/dcl.attr/dcl.attr.nodiscard/p3.cpp (+1-1)
  • (modified) clang/test/OpenMP/declare_variant_messages.cpp (+1-1)
  • (modified) clang/test/Sema/c2x-nodiscard.c (+6-6)
  • (modified) clang/test/Sema/unused-expr.c (+5-5)
  • (modified) clang/test/SemaCXX/coroutines.cpp (+1-1)
  • (modified) clang/test/SemaCXX/warn-unused-result.cpp (+13-13)
  • (modified) clang/test/SemaObjC/method-warn-unused-attribute.m (+3-3)
diff --git a/clang/include/clang/AST/Expr.h b/clang/include/clang/AST/Expr.h
index cbe62411d11bff..8f5679767529fd 100644
--- a/clang/include/clang/AST/Expr.h
+++ b/clang/include/clang/AST/Expr.h
@@ -3182,11 +3182,12 @@ class CallExpr : public Expr {
 
   /// Returns the WarnUnusedResultAttr that is either declared on the called
   /// function, or its return type declaration.
-  const Attr *getUnusedResultAttr(const ASTContext &Ctx) const;
+  std::pair<const NamedDecl *, const Attr *>
+  getUnusedResultAttr(const ASTContext &Ctx) const;
 
   /// Returns true if this call expression should warn on unused results.
   bool hasUnusedResultAttr(const ASTContext &Ctx) const {
-    return getUnusedResultAttr(Ctx) != nullptr;
+    return getUnusedResultAttr(Ctx).second != nullptr;
   }
 
   SourceLocation getRParenLoc() const { return RParenLoc; }
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index c709795e7b21d8..3f6e4c35146454 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -9290,6 +9290,8 @@ def warn_unused_result_typedef_unsupported_spelling : Warning<
 def warn_unused_volatile : Warning<
   "expression result unused; assign into a variable to force a volatile load">,
   InGroup<DiagGroup<"unused-volatile-lvalue">>;
+def note_nodiscard_specified_here : Note<
+  "%0 has been explicitly marked %1 here">;
 
 def ext_cxx14_attr : Extension<
   "use of the %0 attribute is a C++14 extension">, InGroup<CXX14Attrs>;
diff --git a/clang/lib/AST/Expr.cpp b/clang/lib/AST/Expr.cpp
index 9ecbf121e3fc0d..b9450d6a61e57c 100644
--- a/clang/lib/AST/Expr.cpp
+++ b/clang/lib/AST/Expr.cpp
@@ -1616,22 +1616,24 @@ QualType CallExpr::getCallReturnType(const ASTContext &Ctx) const {
   return FnType->getReturnType();
 }
 
-const Attr *CallExpr::getUnusedResultAttr(const ASTContext &Ctx) const {
+std::pair<const NamedDecl *, const Attr *>
+CallExpr::getUnusedResultAttr(const ASTContext &Ctx) const {
   // 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 A;
+      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 A;
+      return {TD->getDecl(), A};
 
   // Otherwise, see if the callee is marked nodiscard and return that attribute
   // instead.
   const Decl *D = getCalleeDecl();
-  return D ? D->getAttr<WarnUnusedResultAttr>() : nullptr;
+  return {dyn_cast<NamedDecl>(D),
+          D ? D->getAttr<WarnUnusedResultAttr>() : nullptr};
 }
 
 SourceLocation CallExpr::getBeginLoc() const {
diff --git a/clang/lib/Sema/SemaStmt.cpp b/clang/lib/Sema/SemaStmt.cpp
index 9e235a46707cd4..baaa6422b88fd4 100644
--- a/clang/lib/Sema/SemaStmt.cpp
+++ b/clang/lib/Sema/SemaStmt.cpp
@@ -14,6 +14,7 @@
 #include "clang/AST/ASTContext.h"
 #include "clang/AST/ASTDiagnostic.h"
 #include "clang/AST/ASTLambda.h"
+#include "clang/AST/Attrs.inc"
 #include "clang/AST/CXXInheritance.h"
 #include "clang/AST/CharUnits.h"
 #include "clang/AST/DeclObjC.h"
@@ -204,23 +205,29 @@ static bool DiagnoseUnusedComparison(Sema &S, const Expr *E) {
   return true;
 }
 
-static bool DiagnoseNoDiscard(Sema &S, const WarnUnusedResultAttr *A,
-                              SourceLocation Loc, SourceRange R1,
-                              SourceRange R2, bool IsCtor) {
+static bool DiagnoseNoDiscard(Sema &S, const NamedDecl *OffendingDecl,
+                              const WarnUnusedResultAttr *A, SourceLocation Loc,
+                              SourceRange R1, SourceRange R2, bool IsCtor) {
   if (!A)
     return false;
   StringRef Msg = A->getMessage();
 
+  bool result;
   if (Msg.empty()) {
     if (IsCtor)
-      return S.Diag(Loc, diag::warn_unused_constructor) << A << R1 << R2;
-    return S.Diag(Loc, diag::warn_unused_result) << A << R1 << R2;
-  }
+      result = S.Diag(Loc, diag::warn_unused_constructor) << A << R1 << R2;
+    else
+      result = S.Diag(Loc, diag::warn_unused_result) << A << R1 << R2;
+  } else if (IsCtor)
+    result = S.Diag(Loc, diag::warn_unused_constructor_msg)
+             << A << Msg << R1 << R2;
+  else
+    result = S.Diag(Loc, diag::warn_unused_result_msg) << A << Msg << R1 << R2;
 
-  if (IsCtor)
-    return S.Diag(Loc, diag::warn_unused_constructor_msg) << A << Msg << R1
-                                                          << R2;
-  return S.Diag(Loc, diag::warn_unused_result_msg) << A << Msg << R1 << R2;
+  if (OffendingDecl)
+    S.Diag(A->getLocation(), diag::note_nodiscard_specified_here)
+        << OffendingDecl << A->getSpelling();
+  return result;
 }
 
 void Sema::DiagnoseUnusedExprResult(const Stmt *S, unsigned DiagID) {
@@ -290,9 +297,12 @@ void Sema::DiagnoseUnusedExprResult(const Stmt *S, unsigned DiagID) {
     if (E->getType()->isVoidType())
       return;
 
-    if (DiagnoseNoDiscard(*this, cast_or_null<WarnUnusedResultAttr>(
-                                     CE->getUnusedResultAttr(Context)),
-                          Loc, R1, R2, /*isCtor=*/false))
+    const NamedDecl *OffendingDecl;
+    const Attr *A;
+    std::tie(OffendingDecl, A) = CE->getUnusedResultAttr(Context);
+    if (DiagnoseNoDiscard(*this, OffendingDecl,
+                          cast_or_null<WarnUnusedResultAttr>(A), Loc, R1, R2,
+                          /*isCtor=*/false))
       return;
 
     // If the callee has attribute pure, const, or warn_unused_result, warn with
@@ -302,27 +312,38 @@ void Sema::DiagnoseUnusedExprResult(const Stmt *S, unsigned DiagID) {
     if (const Decl *FD = CE->getCalleeDecl()) {
       if (ShouldSuppress)
         return;
-      if (FD->hasAttr<PureAttr>()) {
+      if (const auto *A = FD->getAttr<PureAttr>()) {
         Diag(Loc, diag::warn_unused_call) << R1 << R2 << "pure";
+        if (OffendingDecl && !OffendingDecl->getIdentifier()->getBuiltinID())
+          Diag(A->getLocation(), diag::note_nodiscard_specified_here)
+              << OffendingDecl << "pure";
         return;
       }
-      if (FD->hasAttr<ConstAttr>()) {
+      if (const auto *A = FD->getAttr<ConstAttr>()) {
         Diag(Loc, diag::warn_unused_call) << R1 << R2 << "const";
+        if (OffendingDecl && !OffendingDecl->getIdentifier()->getBuiltinID())
+          Diag(A->getLocation(), diag::note_nodiscard_specified_here)
+              << OffendingDecl << "const";
         return;
       }
     }
   } else if (const auto *CE = dyn_cast<CXXConstructExpr>(E)) {
     if (const CXXConstructorDecl *Ctor = CE->getConstructor()) {
+      const NamedDecl *OffendingDecl = Ctor;
       const auto *A = Ctor->getAttr<WarnUnusedResultAttr>();
-      A = A ? A : Ctor->getParent()->getAttr<WarnUnusedResultAttr>();
-      if (DiagnoseNoDiscard(*this, A, Loc, R1, R2, /*isCtor=*/true))
+      if (!A) {
+        OffendingDecl = Ctor->getParent();
+        A = OffendingDecl->getAttr<WarnUnusedResultAttr>();
+      }
+      if (DiagnoseNoDiscard(*this, OffendingDecl, A, Loc, R1, R2,
+                            /*isCtor=*/true))
         return;
     }
   } else if (const auto *ILE = dyn_cast<InitListExpr>(E)) {
     if (const TagDecl *TD = ILE->getType()->getAsTagDecl()) {
 
-      if (DiagnoseNoDiscard(*this, TD->getAttr<WarnUnusedResultAttr>(), Loc, R1,
-                            R2, /*isCtor=*/false))
+      if (DiagnoseNoDiscard(*this, TD, TD->getAttr<WarnUnusedResultAttr>(), Loc,
+                            R1, R2, /*isCtor=*/false))
         return;
     }
   } else if (ShouldSuppress)
@@ -336,8 +357,8 @@ void Sema::DiagnoseUnusedExprResult(const Stmt *S, unsigned DiagID) {
     }
     const ObjCMethodDecl *MD = ME->getMethodDecl();
     if (MD) {
-      if (DiagnoseNoDiscard(*this, MD->getAttr<WarnUnusedResultAttr>(), Loc, R1,
-                            R2, /*isCtor=*/false))
+      if (DiagnoseNoDiscard(*this, MD, MD->getAttr<WarnUnusedResultAttr>(), Loc,
+                            R1, R2, /*isCtor=*/false))
         return;
     }
   } else if (const PseudoObjectExpr *POE = dyn_cast<PseudoObjectExpr>(E)) {
diff --git a/clang/test/CXX/dcl.dcl/dcl.attr/dcl.attr.nodiscard/p2.cpp b/clang/test/CXX/dcl.dcl/dcl.attr/dcl.attr.nodiscard/p2.cpp
index 693ca29370cf3f..158fbdbd67be34 100644
--- a/clang/test/CXX/dcl.dcl/dcl.attr/dcl.attr.nodiscard/p2.cpp
+++ b/clang/test/CXX/dcl.dcl/dcl.attr/dcl.attr.nodiscard/p2.cpp
@@ -2,18 +2,18 @@
 // RUN: %clang_cc1 -fsyntax-only -std=c++17 -verify=expected,cxx11-17,since-cxx17 -pedantic %s
 // RUN: %clang_cc1 -fsyntax-only -std=c++20 -verify=expected,since-cxx17 -pedantic %s
 
-struct [[nodiscard]] S {};
+struct [[nodiscard]] S {}; // expected-note 4 {{'S' has been explicitly marked nodiscard here}}
 // cxx11-warning@-1 {{use of the 'nodiscard' attribute is a C++17 extension}}
 S get_s();
 S& get_s_ref();
 
-enum [[nodiscard]] E {};
+enum [[nodiscard]] E {}; // expected-note 2 {{'E' has been explicitly marked nodiscard here}}
 // cxx11-warning@-1 {{use of the 'nodiscard' attribute is a C++17 extension}}
 E get_e();
 
-[[nodiscard]] int get_i();
+[[nodiscard]] int get_i(); // expected-note {{'get_i' has been explicitly marked nodiscard here}}
 // cxx11-warning@-1 {{use of the 'nodiscard' attribute is a C++17 extension}}
-[[nodiscard]] volatile int &get_vi();
+[[nodiscard]] volatile int &get_vi(); // expected-note {{'get_vi' has been explicitly marked nodiscard here}}
 // cxx11-warning@-1 {{use of the 'nodiscard' attribute is a C++17 extension}}
 
 void f() {
@@ -32,6 +32,7 @@ void f() {
 
 [[nodiscard]] volatile char &(*fp)(); // expected-warning {{'nodiscard' attribute only applies to functions, classes, or enumerations}}
 // cxx11-warning@-1 {{use of the 'nodiscard' attribute is a C++17 extension}}
+// expected-note@-2 {{'fp' has been explicitly marked nodiscard here}}
 void g() {
   fp(); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
 
@@ -67,20 +68,20 @@ void f() {
 }
 } // namespace PR31526
 
-struct [[nodiscard("reason")]] ReasonStruct {};
+struct [[nodiscard("reason")]] ReasonStruct {}; // expected-note {{'ReasonStruct' has been explicitly marked nodiscard here}}
 // cxx11-17-warning@-1 {{use of the 'nodiscard' attribute is a C++20 extension}}
 struct LaterReason;
-struct [[nodiscard("later reason")]] LaterReason {};
+struct [[nodiscard("later reason")]] LaterReason {}; // expected-note {{'LaterReason' has been explicitly marked nodiscard here}}
 // cxx11-17-warning@-1 {{use of the 'nodiscard' attribute is a C++20 extension}}
 
 ReasonStruct get_reason();
 LaterReason get_later_reason();
-[[nodiscard("another reason")]] int another_reason();
+[[nodiscard("another reason")]] int another_reason(); // expected-note {{'another_reason' has been explicitly marked nodiscard here}}
 // cxx11-17-warning@-1 {{use of the 'nodiscard' attribute is a C++20 extension}}
 
 [[nodiscard("conflicting reason")]] int conflicting_reason();
 // cxx11-17-warning@-1 {{use of the 'nodiscard' attribute is a C++20 extension}}
-[[nodiscard("special reason")]] int conflicting_reason();
+[[nodiscard("special reason")]] int conflicting_reason(); // expected-note {{'conflicting_reason' has been explicitly marked nodiscard here}}
 // cxx11-17-warning@-1 {{use of the 'nodiscard' attribute is a C++20 extension}}
 
 void cxx20_use() {
@@ -91,23 +92,23 @@ void cxx20_use() {
 }
 
 namespace p1771 {
-struct[[nodiscard("Don't throw me away!")]] ConvertTo{};
+struct[[nodiscard("Don't throw me away!")]] ConvertTo{}; // expected-note 3 {{'ConvertTo' has been explicitly marked nodiscard here}}
 // cxx11-17-warning@-1 {{use of the 'nodiscard' attribute is a C++20 extension}}
 struct S {
-  [[nodiscard]] S();
+  [[nodiscard]] S(); // expected-note {{'S' has been explicitly marked nodiscard here}}
   // cxx11-warning@-1 {{use of the 'nodiscard' attribute is a C++17 extension}}
-  [[nodiscard("Don't let that S-Char go!")]] S(char);
+  [[nodiscard("Don't let that S-Char go!")]] S(char); // expected-note 2 {{'S' has been explicitly marked nodiscard here}}
   // cxx11-17-warning@-1 {{use of the 'nodiscard' attribute is a C++20 extension}}
   S(int);
   [[gnu::warn_unused_result]] S(double);
   operator ConvertTo();
-  [[nodiscard]] operator int();
+  [[nodiscard]] operator int(); // expected-note 2 {{'operator int' has been explicitly marked nodiscard here}}
   // cxx11-warning@-1 {{use of the 'nodiscard' attribute is a C++17 extension}}
-  [[nodiscard("Don't throw away as a double")]] operator double();
+  [[nodiscard("Don't throw away as a double")]] operator double(); // expected-note {{'operator double' has been explicitly marked nodiscard here}}
   // cxx11-17-warning@-1 {{use of the 'nodiscard' attribute is a C++20 extension}}
 };
 
-struct[[nodiscard("Don't throw me away either!")]] Y{};
+struct[[nodiscard("Don't throw me away either!")]] Y{}; // expected-note {{'Y' has been explicitly marked nodiscard here}}
 // cxx11-17-warning@-1 {{use of the 'nodiscard' attribute is a C++20 extension}}
 
 void usage() {
diff --git a/clang/test/CXX/dcl.dcl/dcl.attr/dcl.attr.nodiscard/p3.cpp b/clang/test/CXX/dcl.dcl/dcl.attr/dcl.attr.nodiscard/p3.cpp
index a3543cff7d2c92..6df334e1c0b488 100644
--- a/clang/test/CXX/dcl.dcl/dcl.attr/dcl.attr.nodiscard/p3.cpp
+++ b/clang/test/CXX/dcl.dcl/dcl.attr/dcl.attr.nodiscard/p3.cpp
@@ -1,7 +1,7 @@
 // RUN: %clang_cc1 -std=c++1z -verify %s
 
 namespace std_example {
-  struct [[nodiscard]] error_info{
+  struct [[nodiscard]] error_info{ // expected-note {{'error_info' has been explicitly marked nodiscard here}}
     // ...
   };
 
diff --git a/clang/test/OpenMP/declare_variant_messages.cpp b/clang/test/OpenMP/declare_variant_messages.cpp
index b8a806e7ef75b3..5fe53d70270fbc 100644
--- a/clang/test/OpenMP/declare_variant_messages.cpp
+++ b/clang/test/OpenMP/declare_variant_messages.cpp
@@ -133,7 +133,7 @@ int score_and_cond_const();
 template <int C>
 int score_and_cond_const_inst();
 
-__attribute__((pure)) int pure() { return 0; }
+__attribute__((pure)) int pure() { return 0; } // expected-note {{'pure' has been explicitly marked pure here}}
 
 #pragma omp declare variant(pure) match(user = {condition(1)}) // expected-warning {{ignoring return value of function declared with pure attribute}}
 int unused_warning_after_specialization() { return foo(); }
diff --git a/clang/test/Sema/c2x-nodiscard.c b/clang/test/Sema/c2x-nodiscard.c
index f8b0567366465d..8865e2f9cd0ca2 100644
--- a/clang/test/Sema/c2x-nodiscard.c
+++ b/clang/test/Sema/c2x-nodiscard.c
@@ -9,7 +9,7 @@ struct [[nodiscard]] S1 { // ok
 struct [[nodiscard, nodiscard]] S2 { // ok
   int i;
 };
-struct [[nodiscard("Wrong")]] S3 {
+struct [[nodiscard("Wrong")]] S3 { // expected-note {{'S3' has been explicitly marked nodiscard here}}
   int i;
 };
 
@@ -20,15 +20,15 @@ enum [[nodiscard]] E1 { One };
 
 [[nodiscard]] int i; // expected-warning {{'nodiscard' attribute only applies to Objective-C methods, enums, structs, unions, classes, functions, function pointers, and typedefs}}
 
-struct [[nodiscard]] S4 {
+struct [[nodiscard]] S4 { // expected-note {{'S4' has been explicitly marked nodiscard here}}
   int i;
 };
 struct S4 get_s(void);
 
-enum [[nodiscard]] E2 { Two };
+enum [[nodiscard]] E2 { Two }; // expected-note {{'E2' has been explicitly marked nodiscard here}}
 enum E2 get_e(void);
 
-[[nodiscard]] int get_i(void);
+[[nodiscard]] int get_i(void); // expected-note {{'get_i' has been explicitly marked nodiscard here}}
 
 void f2(void) {
   get_s(); // expected-warning {{ignoring return value of function declared with 'nodiscard' attribute}}
@@ -43,7 +43,7 @@ void f2(void) {
   (void)get_e();
 }
 
-struct [[nodiscard]] error_info{
+struct [[nodiscard]] error_info{ // expected-note {{'error_info' has been explicitly marked nodiscard here}}
   int i;
 };
 
@@ -54,7 +54,7 @@ void test_missiles(void) {
   launch_missiles();
 }
 
-[[nodiscard]] int f3();
+[[nodiscard]] int f3(); // expected-note {{'f3' has been explicitly marked nodiscard here}}
 
 void GH104391() {
 #define M (unsigned int) f3()
diff --git a/clang/test/Sema/unused-expr.c b/clang/test/Sema/unused-expr.c
index 6723a33cbd4e01..9e444f94d67137 100644
--- a/clang/test/Sema/unused-expr.c
+++ b/clang/test/Sema/unused-expr.c
@@ -73,15 +73,15 @@ void t4(int a) {
   for (;;b < 1) {} // expected-warning{{relational comparison result unused}}
 }
 
-int t5f(void) __attribute__((warn_unused_result));
+int t5f(void) __attribute__((warn_unused_result)); // expected-note {{'t5f' has been explicitly marked warn_unused_result here}}
 void t5(void) {
   t5f();   // expected-warning {{ignoring return value of function declared with 'warn_unused_result' attribute}}
 }
 
 
-int fn1(void) __attribute__ ((warn_unused_result));
-int fn2() __attribute__ ((pure));
-int fn3() __attribute__ ((__const));
+int fn1(void) __attribute__ ((warn_unused_result)); // expected-note 3 {{'fn1' has been explicitly marked warn_unused_result here}}
+int fn2() __attribute__ ((pure)); // expected-note 2 {{'fn2' has been explicitly marked pure here}}
+int fn3() __attribute__ ((__const)); // expected-note {{'fn3' has been explicitly marked const here}}
 int t6(void) {
   if (fn1() < 0 || fn2(2,1) < 0 || fn3(2) < 0)  // no warnings
     return -1;
@@ -97,7 +97,7 @@ int t6(void) {
 int t7 __attribute__ ((warn_unused_result)); // expected-warning {{'warn_unused_result' attribute only applies to Objective-C methods, enums, structs, unions, classes, functions, function pointers, and typedefs}}
 
 // PR4010
-int (*fn4)(void) __attribute__ ((warn_unused_result));
+int (*fn4)(void) __attribute__ ((warn_unused_result)); // expected-note {{'fn4' has been explicitly marked warn_unused_result here}}
 void t8(void) {
   fn4(); // expected-warning {{ignoring return value of function declared with 'warn_unused_result' attribute}}
 }
diff --git a/clang/test/SemaCXX/coroutines.cpp b/clang/test/SemaCXX/coroutines.cpp
index 068fdab4bfe384..c9d442a0338121 100644
--- a/clang/test/SemaCXX/coroutines.cpp
+++ b/clang/test/SemaCXX/coroutines.cpp
@@ -1497,7 +1497,7 @@ class awaitable_unused_warn {
   using handle_type = std::coroutine_handle<>;
   constexpr bool await_ready() noexcept { return false; }
   void await_suspend(handle_type) noexcept {}
-  [[nodiscard]] int await_resume() noexcept { return 1; }
+  [[nodiscard]] int await_resume() noexcept { return 1; } // expected-note 2 {{'await_resume' has been explicitly marked nodiscard here}}
 };
 
 template <class Await>
diff --git a/clang/test/SemaCXX/warn-unused-result.cpp b/clang/test/SemaCXX/warn-unused-result.cpp
index 4b7a2503ecc0dd..247be530274af2 100644
--- a/clang/test/SemaCXX/warn-unused-result.cpp
+++ b/clang/test/SemaCXX/warn-unused-result.cpp
@@ -1,13 +1,13 @@
 // RUN: %clang_cc1 -fsyntax-only -verify -std=c++11 %s
 
-int f() __attribute__((warn_unused_result));
+int f() __attribute__((warn_unused_result)); // expected-note 12 {{'f' has been explicitly marked warn_unused_result here}}
 
 struct S {
   void t() const;
 };
-S g1() __attribute__((warn_unused_result));
-S *g2() __attribute__((warn_unused_result));
-S &g3() __attribute__((warn_unused_result));
+S g1() __attribute__((warn_unused_result)); // expected-note {{'g1' has been explicitly marked warn_unused_result here}}
+S *g2() __attribute__((warn_unused_result)); // expected-note {{'g2' has been explicitly marked warn_unused_result here}}
+S &g3() __attribute__((warn_unused_result)); // expected-note {{'g3' has been explicitly marked warn_unused_result here}}
 
 void test() {
   f(); // expected-warning {{ignoring return value}}
@@ -64,7 +64,7 @@ void testSubstmts(int i) {
 }
 
 struct X {
- int foo() __attribute__((warn_unused_result));
+ int foo() __attribute__((warn_unused_result)); // expected-note 2 {{'foo' has been explicitly marked warn_unused_result here}}
 };
 
 void bah() {
@@ -80,7 +80,7 @@ class Foo {
   Status doStuff();
 };
 
-struct [[clang::warn_unused_result]] Status {
+struct [[clang::warn_unused_result]] Status { // expected-note 3 {{'Status' has been explicitly marked warn_unused_result here}}
   bool ok() const;
   Status& operator=(const Status& x);
   inline void Update(const Status& new_status) {
@@ -115,7 +115,7 @@ void lazy() {
 }
 
 template ...
[truncated]

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.

Hmm, I can’t really think of a situation when this note would actually help... at least for deprecated, you could maybe argue that you might want to know what part of the codebase marked it as deprecated in case there are multiple declarations, but for nodiscard, I don’t think you really care that much, so I feel like this might just be adding noise instead of helping. Imo just the error is enough, but that’s just my opinion.

@Sirraide
Copy link
Member

There seems to exist a discrepancy between [[deprecated]] and [[nodiscard]]. The former generates a warning on the usage of the function, together with a note on the declaration of the function. In contrast, the latter only generates a warning.

As I said, I feel like that might be by design, though.

No new test cases are added, as I felt that adding expected-notes on existing test cases already serves as testing for this PR.

Yeah, there are already enough tests for this.

@Sirraide
Copy link
Member

Sirraide commented Oct 15, 2024

for nodiscard, I don’t think you really care that much

Ah, 1 more case that comes to mind for deprecated is if you have several overloads, and only some of them are deprecated. I don’t really see that happening for nodiscard tho—making only some overloads nodiscard instead of none or all of them just seems like a really weird thing to do personally...

@erichkeane
Copy link
Collaborator

Hmm, I can’t really think of a situation when this note would actually help... at least for deprecated, you could maybe argue that you might want to know what part of the codebase marked it as deprecated in case there are multiple declarations, but for nodiscard, I don’t think you really care that much, so I feel like this might just be adding noise instead of helping. Imo just the error is enough, but that’s just my opinion.

The one use case I could see is that the nodiscard is on the TYPE instead of the function itself (which, as you said before, is goofy for an overload set with this only partially applied). But I don't really see value besides "this should not be discarded", and the actual location of the attribute isn't particularly valuable. What IS important is the 'reason', which we already have.

So this is definitely causing me troubles with 'motivation'.

@Sirraide Sirraide removed the clang:openmp OpenMP related changes to Clang label Oct 15, 2024
@Mick235711
Copy link
Contributor Author

I don’t really see that happening for nodiscard tho—making only some overloads nodiscard instead of none or all of them just seems like a really weird thing to do personally...

I think there is still some merits for these kind of overload sets. For instance, API like basic_spanstream::span(), where the same name is used for both setter and getter, is pretty common out in the wild:

std::span<CharT> span() const noexcept;
void span( std::span<CharT> s ) noexcept;

In these kind of cases the first overload can be marked [[nodiscard]], which is probably reasonable.

As I said, I feel like that might be by design, though.

at least for deprecated, you could maybe argue that you might want to know what part of the codebase marked it as deprecated in case there are multiple declarations

Indeed, I'm currently torn about this. On the one hand, the same argument can be applied to nodiscard, where you might want to know what part of the codebase marked it as nodiscard in case of multiple declarations. On the other hand, the motivation is definitely weaker here.

The one use case I could see is that the nodiscard is on the TYPE instead of the function itself (which, as you said before, is goofy for an overload set with this only partially applied). But I don't really see value besides "this should not be discarded", and the actual location of the attribute isn't particularly valuable. What IS important is the 'reason', which we already have.

I can suggest one motivation here, which potentially applies to [[nodiscard]] on types. In the standard, it is allowed to mark only some specializations of a class template as nodiscard:

template<typename T> struct S {};
template<> struct [[nodiscard]] S<int> {};

template<typename T>
S<T> getS(const T&) { return {}; }

int main()
{
    getS(2.0); // no warn
    getS(2); // warns
}

In this case, the current warning is not especially good, the only thing mentioned is: (Compiler Explorer)

<source>:10:5: warning: ignoring return value of function declared with 'nodiscard' attribute [-Wunused-result]
   10 |     getS(2);
      |     ^~~~ ~
1 warning generated.

Nowhere is the return type S actually mentioned, let alone the specific specialization. Under this PR, this will instead generate:

test-2.cpp:10:5: warning: ignoring return value of function declared with 'nodiscard' attribute [-Wunused-result]
   10 |     getS(2);
      |     ^~~~ ~
test-2.cpp:2:21: note: 'S<int>' has been explicitly marked nodiscard here
    2 | template<> struct [[nodiscard]] S<int> {};
      |                     ^
1 warning generated.

Here the specialization is named, which is potentially helpful. Although I'll admit this is a bit of a contrived example.

The noise argument does makes sense though, so I'm currently undecided about this too.

@Sirraide
Copy link
Member

Here the specialization is named, which is potentially helpful.

Hmm, the diagnostic for the error here isn’t particularly great to begin with; I feel like it should instead mention that the return type, not the function, is marked nodiscard, which might be enough to fix the situation for partial specialisations?

@Sirraide
Copy link
Member

Sirraide commented Oct 15, 2024

I.e. could we get it to emit something like:

<source>:10:5: warning: ignoring return value of type 'S<int>' declared with 'nodiscard' attribute [-Wunused-result]
   10 |     getS(2);
      |     ^~~~ ~
1 warning generated.

That is, keep the current wording if the function is nodiscard, but change it to mention the type instead if the type is marked nodiscard—provided there is a relatively straight-forward way of doing this.

@Mick235711
Copy link
Contributor Author

That is, keep the current wording if the function is nodiscard, but change it to mention the type instead if the type is marked nodiscard—provided there is a relatively straight-forward way of doing this.

I think this should be okay-ish to implement since we basically already have that information when implementing the note anyway. Will try to implement this tomorrow, if I can find some time...

@llvmbot llvmbot added the clang:openmp OpenMP related changes to Clang label Oct 15, 2024
StringRef type = (isa<PureAttr>(A) ? "pure" : "const");
Diag(Loc, diag::warn_unused_call) << R1 << R2 << type;
if (const auto *ND = dyn_cast<NamedDecl>(OffendingDecl)) {
if (!ND->getIdentifier()->getBuiltinID())
Copy link
Collaborator

Choose a reason for hiding this comment

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

I still didn't get the justification for skipping if this is a builtin? It seems sensible to do so, even if the source location is a builtin.

Copy link
Contributor Author

@Mick235711 Mick235711 Oct 15, 2024

Choose a reason for hiding this comment

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

As I said above, the real motivation for avoiding marking here is twofold:

  1. Builtins like isdigit are just marked pure internally, the note "explicitly marked pure/const here" is just wrong, there is no such attribute on that line.
int isdigit(int c) __attribute__((overloadable)); // The note actually fires on this line, which doesn't have a pure attribute
int isdigit(int c) __attribute__((overloadable)) // expected-note {{'isdigit' has been explicitly marked unavailable here}}
  __attribute__((enable_if(c <= -1 || c > 255, "'c' must have the value of an unsigned char or EOF")))
  __attribute__((unavailable("'c' must have the value of an unsigned char or EOF")));

void test3(int c) {
  isdigit(c); // expected-warning{{ignoring return value of function declared with pure attribute}}
  isdigit(10); // expected-warning{{ignoring return value of function declared with pure attribute}}
#ifndef CODEGEN
  isdigit(-10);  // expected-error{{'isdigit' is unavailable: 'c' must have the value of an unsigned char or EOF}}
#endif
}

Unless A->getLocation() is not the right invocation to find the attribute location, I don't think there is a "builtin" declaration line that can be referenced by the note.

  1. Language builtins like __builtin_operator_new will just fire the note on the first use:
void test_typo_in_args() {
  __builtin_operator_new(DNE);          // expected-error {{undeclared identifier 'DNE'}}
  __builtin_operator_new(DNE, DNE2);    // expected-error {{undeclared identifier 'DNE'}} expected-error {{'DNE2'}}
  __builtin_operator_delete(DNE);       // expected-error {{'DNE'}}
  __builtin_operator_delete(DNE, DNE2); // expected-error {{'DNE'}} expected-error {{'DNE2'}}
}

In this test case (SemaCXX/builtin-operator-new-delete.cpp), the note is generated on the second line, i.e. first use of builtin, which is even more wrong as it is not even a declaration.

Excluding builtins from note generation fixes both errors, I think...

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm... thats actually pretty strange: https://godbolt.org/z/q6EMMs6fa
If you make it NOT an error for the first declaration, it does what I suspect it SHOULD be doing in all cases, creating the overload set without a location: https://godbolt.org/z/4zeq99bne

So there is perhaps a bug in the builtin recovery we need to understand before we can move on.

Sirraide added a commit that referenced this pull request Nov 19, 2024
A follow-up to #112289.

When diagnosing an unused return value, if the diagnostic
is triggered by an attribute attached to a type, the type name
is now included in the diagnostic.

---------

Co-authored-by: Sirraide <[email protected]>
@Mick235711
Copy link
Contributor Author

Closed in favor of #112521.

@Mick235711 Mick235711 closed this Nov 19, 2024
searlmc1 pushed a commit to ROCm/llvm-project that referenced this pull request Dec 13, 2024
A follow-up to llvm#112289.

When diagnosing an unused return value, if the diagnostic
is triggered by an attribute attached to a type, the type name
is now included in the diagnostic.

---------

Co-authored-by: Sirraide <[email protected]>
Change-Id: I03f6ee4fde4b9df42e5b3b56881d28ccaeccb5b7
searlmc1 pushed a commit to ROCm/llvm-project that referenced this pull request Mar 29, 2025
A follow-up to llvm#112289.

When diagnosing an unused return value, if the diagnostic
is triggered by an attribute attached to a type, the type name
is now included in the diagnostic.

---------

Co-authored-by: Sirraide <[email protected]>
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:openmp OpenMP related changes to Clang clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants