Skip to content

Conversation

@fmayer
Copy link
Contributor

@fmayer fmayer commented Dec 8, 2025

The mock headers are copied from clang/unittests/Analysis/FlowSensitive/MockHeaders.cpp.

Relevant RFC: https://discourse.llvm.org/t/rfc-abseil-unchecked-statusor-use-check/87998

Created using spr 1.3.7
@llvmbot
Copy link
Member

llvmbot commented Dec 8, 2025

@llvm/pr-subscribers-clang-tools-extra

Author: Florian Mayer (fmayer)

Changes

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

4 Files Affected:

  • (modified) clang-tools-extra/clang-tidy/abseil/AbseilTidyModule.cpp (+3)
  • (modified) clang-tools-extra/clang-tidy/abseil/CMakeLists.txt (+1)
  • (added) clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.cpp (+63)
  • (added) clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.h (+25)
diff --git a/clang-tools-extra/clang-tidy/abseil/AbseilTidyModule.cpp b/clang-tools-extra/clang-tidy/abseil/AbseilTidyModule.cpp
index 8971530bab9b2..0ecfb25f31515 100644
--- a/clang-tools-extra/clang-tidy/abseil/AbseilTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/abseil/AbseilTidyModule.cpp
@@ -27,6 +27,7 @@
 #include "StringFindStrContainsCheck.h"
 #include "TimeComparisonCheck.h"
 #include "TimeSubtractionCheck.h"
+#include "UncheckedStatusOrAccessCheck.h"
 #include "UpgradeDurationConversionsCheck.h"
 
 namespace clang::tidy {
@@ -69,6 +70,8 @@ class AbseilModule : public ClangTidyModule {
         "abseil-time-subtraction");
     CheckFactories.registerCheck<UpgradeDurationConversionsCheck>(
         "abseil-upgrade-duration-conversions");
+        CheckFactories.registerCheck<UncheckedStatusOrAccessCheck>(
+        "abseil-unchecked-statusor-access");
   }
 };
 
diff --git a/clang-tools-extra/clang-tidy/abseil/CMakeLists.txt b/clang-tools-extra/clang-tidy/abseil/CMakeLists.txt
index ca7cc6782f1e6..0c02ffc7306d1 100644
--- a/clang-tools-extra/clang-tidy/abseil/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/abseil/CMakeLists.txt
@@ -25,6 +25,7 @@ add_clang_library(clangTidyAbseilModule STATIC
   TimeComparisonCheck.cpp
   TimeSubtractionCheck.cpp
   UpgradeDurationConversionsCheck.cpp
+  UncheckedStatusOrAccessCheck.cpp
 
   LINK_LIBS
   clangTidy
diff --git a/clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.cpp b/clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.cpp
new file mode 100644
index 0000000000000..d8842cb2b55a2
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.cpp
@@ -0,0 +1,63 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "UncheckedStatusOrAccessCheck.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
+#include "clang/Analysis/FlowSensitive/Models/UncheckedStatusOrAccessModel.h"
+#include "clang/Basic/SourceLocation.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/Support/Error.h"
+
+namespace clang::tidy::abseil {
+using ast_matchers::MatchFinder;
+using dataflow::statusor_model::UncheckedStatusOrAccessModel;
+using dataflow::statusor_model::UncheckedStatusOrAccessDiagnoser;
+
+static constexpr llvm::StringLiteral FuncID("fun");
+
+void UncheckedStatusOrAccessCheck::registerMatchers(MatchFinder *Finder) {
+  using namespace ast_matchers;
+  if (!getLangOpts().CPlusPlus) return;
+
+  auto has_statusor_call_descendant =
+      hasDescendant(callExpr(callee(cxxMethodDecl(ofClass(hasAnyName(
+          "absl::StatusOr", "absl::internal_statusor::OperatorBase"))))));
+  Finder->addMatcher(functionDecl(unless(isExpansionInSystemHeader()),
+                                  hasBody(has_statusor_call_descendant))
+                         .bind(FuncID),
+                     this);
+  Finder->addMatcher(
+      cxxConstructorDecl(hasAnyConstructorInitializer(
+                             withInitializer(has_statusor_call_descendant)))
+          .bind(FuncID),
+      this);
+}
+void UncheckedStatusOrAccessCheck::check(
+    const MatchFinder::MatchResult &Result) {
+  if (Result.SourceManager->getDiagnostics().hasUncompilableErrorOccurred())
+    return;
+
+  const auto *FuncDecl = Result.Nodes.getNodeAs<FunctionDecl>(FuncID);
+  if (FuncDecl->isTemplated())
+    return;
+
+  UncheckedStatusOrAccessDiagnoser Diagnoser;
+  if (llvm::Expected<llvm::SmallVector<SourceLocation>> Locs =
+          dataflow::diagnoseFunction<
+              UncheckedStatusOrAccessModel,
+              SourceLocation>(*FuncDecl, *Result.Context, Diagnoser))
+    for (const SourceLocation& Loc : *Locs)
+      diag(Loc, "unchecked access to 'absl::StatusOr' value");
+  else
+    llvm::consumeError(Locs.takeError());
+}
+
+} // namespace clang::tidy::abseil
diff --git a/clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.h b/clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.h
new file mode 100644
index 0000000000000..97ecd1befd6ee
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.h
@@ -0,0 +1,25 @@
+#ifndef DEVTOOLS_CYMBAL_CLANG_TIDY_RUNTIME_UPSTREAM_FRAMEWORK_UNCHECKED_STATUSOR_ACCESS_H_
+#define DEVTOOLS_CYMBAL_CLANG_TIDY_RUNTIME_UPSTREAM_FRAMEWORK_UNCHECKED_STATUSOR_ACCESS_H_
+
+#include <optional>
+
+#include "../ClangTidyCheck.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+
+namespace clang::tidy::abseil {
+
+// Warns when the code is unwrapping an absl::StatusOr<T> object without
+// assuring that it contains a value.
+//
+// For details on the dataflow analysis implemented in this check see:
+// http://google3/devtools/cymbal/nullability/statusor
+class UncheckedStatusOrAccessCheck : public ClangTidyCheck {
+ public:
+  using ClangTidyCheck::ClangTidyCheck;
+  void registerMatchers(ast_matchers::MatchFinder* Finder) override;
+  void check(const ast_matchers::MatchFinder::MatchResult& Result) override;
+};
+
+}  // namespace clang::abseil
+
+#endif  // DEVTOOLS_CYMBAL_CLANG_TIDY_RUNTIME_UPSTREAM_FRAMEWORK_UNCHECKED_STATUSOR_ACCESS_H_

@llvmbot
Copy link
Member

llvmbot commented Dec 8, 2025

@llvm/pr-subscribers-clang-tidy

Author: Florian Mayer (fmayer)

Changes

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

4 Files Affected:

  • (modified) clang-tools-extra/clang-tidy/abseil/AbseilTidyModule.cpp (+3)
  • (modified) clang-tools-extra/clang-tidy/abseil/CMakeLists.txt (+1)
  • (added) clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.cpp (+63)
  • (added) clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.h (+25)
diff --git a/clang-tools-extra/clang-tidy/abseil/AbseilTidyModule.cpp b/clang-tools-extra/clang-tidy/abseil/AbseilTidyModule.cpp
index 8971530bab9b2..0ecfb25f31515 100644
--- a/clang-tools-extra/clang-tidy/abseil/AbseilTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/abseil/AbseilTidyModule.cpp
@@ -27,6 +27,7 @@
 #include "StringFindStrContainsCheck.h"
 #include "TimeComparisonCheck.h"
 #include "TimeSubtractionCheck.h"
+#include "UncheckedStatusOrAccessCheck.h"
 #include "UpgradeDurationConversionsCheck.h"
 
 namespace clang::tidy {
@@ -69,6 +70,8 @@ class AbseilModule : public ClangTidyModule {
         "abseil-time-subtraction");
     CheckFactories.registerCheck<UpgradeDurationConversionsCheck>(
         "abseil-upgrade-duration-conversions");
+        CheckFactories.registerCheck<UncheckedStatusOrAccessCheck>(
+        "abseil-unchecked-statusor-access");
   }
 };
 
diff --git a/clang-tools-extra/clang-tidy/abseil/CMakeLists.txt b/clang-tools-extra/clang-tidy/abseil/CMakeLists.txt
index ca7cc6782f1e6..0c02ffc7306d1 100644
--- a/clang-tools-extra/clang-tidy/abseil/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/abseil/CMakeLists.txt
@@ -25,6 +25,7 @@ add_clang_library(clangTidyAbseilModule STATIC
   TimeComparisonCheck.cpp
   TimeSubtractionCheck.cpp
   UpgradeDurationConversionsCheck.cpp
+  UncheckedStatusOrAccessCheck.cpp
 
   LINK_LIBS
   clangTidy
diff --git a/clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.cpp b/clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.cpp
new file mode 100644
index 0000000000000..d8842cb2b55a2
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.cpp
@@ -0,0 +1,63 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "UncheckedStatusOrAccessCheck.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
+#include "clang/Analysis/FlowSensitive/Models/UncheckedStatusOrAccessModel.h"
+#include "clang/Basic/SourceLocation.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/Support/Error.h"
+
+namespace clang::tidy::abseil {
+using ast_matchers::MatchFinder;
+using dataflow::statusor_model::UncheckedStatusOrAccessModel;
+using dataflow::statusor_model::UncheckedStatusOrAccessDiagnoser;
+
+static constexpr llvm::StringLiteral FuncID("fun");
+
+void UncheckedStatusOrAccessCheck::registerMatchers(MatchFinder *Finder) {
+  using namespace ast_matchers;
+  if (!getLangOpts().CPlusPlus) return;
+
+  auto has_statusor_call_descendant =
+      hasDescendant(callExpr(callee(cxxMethodDecl(ofClass(hasAnyName(
+          "absl::StatusOr", "absl::internal_statusor::OperatorBase"))))));
+  Finder->addMatcher(functionDecl(unless(isExpansionInSystemHeader()),
+                                  hasBody(has_statusor_call_descendant))
+                         .bind(FuncID),
+                     this);
+  Finder->addMatcher(
+      cxxConstructorDecl(hasAnyConstructorInitializer(
+                             withInitializer(has_statusor_call_descendant)))
+          .bind(FuncID),
+      this);
+}
+void UncheckedStatusOrAccessCheck::check(
+    const MatchFinder::MatchResult &Result) {
+  if (Result.SourceManager->getDiagnostics().hasUncompilableErrorOccurred())
+    return;
+
+  const auto *FuncDecl = Result.Nodes.getNodeAs<FunctionDecl>(FuncID);
+  if (FuncDecl->isTemplated())
+    return;
+
+  UncheckedStatusOrAccessDiagnoser Diagnoser;
+  if (llvm::Expected<llvm::SmallVector<SourceLocation>> Locs =
+          dataflow::diagnoseFunction<
+              UncheckedStatusOrAccessModel,
+              SourceLocation>(*FuncDecl, *Result.Context, Diagnoser))
+    for (const SourceLocation& Loc : *Locs)
+      diag(Loc, "unchecked access to 'absl::StatusOr' value");
+  else
+    llvm::consumeError(Locs.takeError());
+}
+
+} // namespace clang::tidy::abseil
diff --git a/clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.h b/clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.h
new file mode 100644
index 0000000000000..97ecd1befd6ee
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/abseil/UncheckedStatusOrAccessCheck.h
@@ -0,0 +1,25 @@
+#ifndef DEVTOOLS_CYMBAL_CLANG_TIDY_RUNTIME_UPSTREAM_FRAMEWORK_UNCHECKED_STATUSOR_ACCESS_H_
+#define DEVTOOLS_CYMBAL_CLANG_TIDY_RUNTIME_UPSTREAM_FRAMEWORK_UNCHECKED_STATUSOR_ACCESS_H_
+
+#include <optional>
+
+#include "../ClangTidyCheck.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+
+namespace clang::tidy::abseil {
+
+// Warns when the code is unwrapping an absl::StatusOr<T> object without
+// assuring that it contains a value.
+//
+// For details on the dataflow analysis implemented in this check see:
+// http://google3/devtools/cymbal/nullability/statusor
+class UncheckedStatusOrAccessCheck : public ClangTidyCheck {
+ public:
+  using ClangTidyCheck::ClangTidyCheck;
+  void registerMatchers(ast_matchers::MatchFinder* Finder) override;
+  void check(const ast_matchers::MatchFinder::MatchResult& Result) override;
+};
+
+}  // namespace clang::abseil
+
+#endif  // DEVTOOLS_CYMBAL_CLANG_TIDY_RUNTIME_UPSTREAM_FRAMEWORK_UNCHECKED_STATUSOR_ACCESS_H_

@github-actions
Copy link

github-actions bot commented Dec 8, 2025

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

Created using spr 1.3.7
@github-actions
Copy link

github-actions bot commented Dec 8, 2025

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

Created using spr 1.3.7
Created using spr 1.3.7
Created using spr 1.3.7
@fmayer fmayer requested review from jvoung and ymand December 9, 2025 01:53
@fmayer fmayer marked this pull request as ready for review December 9, 2025 01:53
Created using spr 1.3.7
Created using spr 1.3.7
Created using spr 1.3.7
@fmayer fmayer requested a review from vbvictor December 10, 2025 22:08
Copy link
Contributor

@jvoung jvoung left a comment

Choose a reason for hiding this comment

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

mostly nits, otherwise nice work!

False negatives
---------------

This check generally does **not** generate false negatives. If it cannot
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a better phrasing than "generate" false negatives? If the check has false negatives then it won't generate any report?

Also, the "If it cannot prove an access safe..." do you mean the opposite? "If it cannot prove an access unsafe..." ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a better phrasing than "generate" false negatives? If the check has false negatives then it won't generate any report?

I don't know. It does not have false negatives, which means if a StatusOr access is deemed safe by the analysis, it is safe. If it is deemed unsafe, it could still be safe.

Also, the "If it cannot prove an access safe..." do you mean the opposite? "If it cannot prove an access unsafe..." ?

No; what is written is correct. This might also have caused the confusion for the other question?

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, the second sentence is describing how the checker works:
!isSafeUnwrap(...) then issue warning that it is unsafe.

Whether that "unsafe" is true or not made me think more of the "false positive" category so was a bit confusing.

Perhaps you can clarify that and "note that if it is deemed unsafe, it could still be safe (false positive)."... or something to that effect.

Use ``ABSL_CHECK_OK`` to signal that you knowingly want to crash on
non-OK values.

NOTE: Even though using ``.value()`` on a ``nullopt`` is defined to crash,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is nullopt the right term for StatusOr, or something more like "no value is present" / "status is not ok"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

}
void f(Foo foo) {
if (foo.get().ok()) {
use(*get.get());
Copy link
Contributor

Choose a reason for hiding this comment

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

get.get() -> foo.get()

same below for the mutate() case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

}

void function_template_user(const absl::StatusOr<int>& sor) {
// Instantiate the f3 function template so that it gets matched by the check.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: f3 is renamed to "function_template_with_user" ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

void multiple_unchecked_accesses(absl::StatusOr<int> sor1,
absl::StatusOr<int> sor2) {
for (int i = 0; i < 10; i++) {
sor1.ValueOrDie();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: could be simpler to use value() for these tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Created using spr 1.3.7
Copy link
Contributor Author

@fmayer fmayer left a comment

Choose a reason for hiding this comment

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

also realised the test had the wrong name, removed the abseil from the file name because it's already in the directory name

False negatives
---------------

This check generally does **not** generate false negatives. If it cannot
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a better phrasing than "generate" false negatives? If the check has false negatives then it won't generate any report?

I don't know. It does not have false negatives, which means if a StatusOr access is deemed safe by the analysis, it is safe. If it is deemed unsafe, it could still be safe.

Also, the "If it cannot prove an access safe..." do you mean the opposite? "If it cannot prove an access unsafe..." ?

No; what is written is correct. This might also have caused the confusion for the other question?

}
void f(Foo foo) {
if (foo.get().ok()) {
use(*get.get());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

}

void function_template_user(const absl::StatusOr<int>& sor) {
// Instantiate the f3 function template so that it gets matched by the check.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

void multiple_unchecked_accesses(absl::StatusOr<int> sor1,
absl::StatusOr<int> sor2) {
for (int i = 0; i < 10; i++) {
sor1.ValueOrDie();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@fmayer fmayer requested a review from jvoung December 11, 2025 04:12
Created using spr 1.3.7
False negatives
---------------

This check generally does **not** generate false negatives. If it cannot
Copy link
Contributor

Choose a reason for hiding this comment

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

I see, the second sentence is describing how the checker works:
!isSafeUnwrap(...) then issue warning that it is unsafe.

Whether that "unsafe" is true or not made me think more of the "false positive" category so was a bit confusing.

Perhaps you can clarify that and "note that if it is deemed unsafe, it could still be safe (false positive)."... or something to that effect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants