[clang-tidy] Add check to replace operator[] with at() Enforce SL.con.3 (#95220)

This PR is based on the PR #90043 by Sebastian Wolf who has given us
(Manuel and myself) permission to continue his work

The original PR message reads:

> The string based test to find out whether the check is applicable on
the class is not ideal, but I did not find a more elegant way, yet.
> If there is more clang-query magic available, that I'm not aware of,
I'm happy to adapt that.

As part of the reviews for that PR, Sebastian changed the following:

- Detect viable classes automatically instead of looking for fixed names
- Disable fix-it suggestions

This PR contains the same changes and, in addition, addresses further
feedback provided by the maintainers.

Changes in addition to the original PR:
- Exclude `std::map`, `std::flat_map`, and `std::unordered_map` from the
analysis by default, and add the ability for users to exclude additional
classes from the analysis
- Add the tests Piotr Zegar requested
- Rename the analysis from AvoidBoundsErrorsCheck to
PreferAtOverSubscriptOperatorCheck as requested by Piotr
- Add a more detailed description of what the analysis does as requested
by Piotr

We explicitly don't ignore unused code with
`TK_IgnoreUnlessSpelledInSource`, as requested by Piotr,
because it caused the template-related tests to fail.

We are not sure what the best behaviour for templates is; should we:

- not warn if using `at()` will make a different instantiation not
compile?
- warn at the place that requires the template instantiation?
- keep the warning and add the name of the class of the object / the
template parameters that lead to the message?
- not warn in templates at all because the code is implicit?

Carlos Galvez and Congcong Cai discussed the possibility of disabling
the check when exceptions are disabled, but we were unsure whether
they'd reached a conclusion and whether it was still relevant when
fix-it suggestions are disabled.

What do you think?

Co-authored-by: Manuel Pietsch <manuelpietsch@outlook.de>
Co-authored-by: Sebastian Wolf <wolf.sebastian@in.tum.de>

---------

Co-authored-by: EugeneZelenko <eugene.zelenko@gmail.com>
Co-authored-by: Baranov Victor <bar.victor.2002@gmail.com>
GitOrigin-RevId: bc278d00726fc7f37b3c6e6b1d0556f581f0865b
diff --git a/clang-tidy/cppcoreguidelines/CMakeLists.txt b/clang-tidy/cppcoreguidelines/CMakeLists.txt
index 2fb4d7f..0abb000 100644
--- a/clang-tidy/cppcoreguidelines/CMakeLists.txt
+++ b/clang-tidy/cppcoreguidelines/CMakeLists.txt
@@ -21,6 +21,7 @@
   OwningMemoryCheck.cpp
   PreferMemberInitializerCheck.cpp
   ProBoundsArrayToPointerDecayCheck.cpp
+  ProBoundsAvoidUncheckedContainerAccess.cpp
   ProBoundsConstantArrayIndexCheck.cpp
   ProBoundsPointerArithmeticCheck.cpp
   ProTypeConstCastCheck.cpp
diff --git a/clang-tidy/cppcoreguidelines/CppCoreGuidelinesTidyModule.cpp b/clang-tidy/cppcoreguidelines/CppCoreGuidelinesTidyModule.cpp
index 4b3b7bf..cc1ae15 100644
--- a/clang-tidy/cppcoreguidelines/CppCoreGuidelinesTidyModule.cpp
+++ b/clang-tidy/cppcoreguidelines/CppCoreGuidelinesTidyModule.cpp
@@ -36,6 +36,7 @@
 #include "OwningMemoryCheck.h"
 #include "PreferMemberInitializerCheck.h"
 #include "ProBoundsArrayToPointerDecayCheck.h"
+#include "ProBoundsAvoidUncheckedContainerAccess.h"
 #include "ProBoundsConstantArrayIndexCheck.h"
 #include "ProBoundsPointerArithmeticCheck.h"
 #include "ProTypeConstCastCheck.h"
@@ -107,6 +108,8 @@
         "cppcoreguidelines-prefer-member-initializer");
     CheckFactories.registerCheck<ProBoundsArrayToPointerDecayCheck>(
         "cppcoreguidelines-pro-bounds-array-to-pointer-decay");
+    CheckFactories.registerCheck<ProBoundsAvoidUncheckedContainerAccess>(
+        "cppcoreguidelines-pro-bounds-avoid-unchecked-container-access");
     CheckFactories.registerCheck<ProBoundsConstantArrayIndexCheck>(
         "cppcoreguidelines-pro-bounds-constant-array-index");
     CheckFactories.registerCheck<ProBoundsPointerArithmeticCheck>(
diff --git a/clang-tidy/cppcoreguidelines/ProBoundsAvoidUncheckedContainerAccess.cpp b/clang-tidy/cppcoreguidelines/ProBoundsAvoidUncheckedContainerAccess.cpp
new file mode 100644
index 0000000..35f432e
--- /dev/null
+++ b/clang-tidy/cppcoreguidelines/ProBoundsAvoidUncheckedContainerAccess.cpp
@@ -0,0 +1,262 @@
+//===--- ProBoundsAvoidUncheckedContainerAccess.cpp - clang-tidy ----------===//
+//
+// 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 "ProBoundsAvoidUncheckedContainerAccess.h"
+#include "../utils/Matchers.h"
+#include "../utils/OptionsUtils.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "llvm/ADT/StringRef.h"
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::cppcoreguidelines {
+
+static constexpr llvm::StringRef DefaultExclusionStr =
+    "::std::map;::std::unordered_map;::std::flat_map";
+
+ProBoundsAvoidUncheckedContainerAccess::ProBoundsAvoidUncheckedContainerAccess(
+    StringRef Name, ClangTidyContext *Context)
+    : ClangTidyCheck(Name, Context),
+      ExcludedClasses(utils::options::parseStringList(
+          Options.get("ExcludeClasses", DefaultExclusionStr))),
+      FixMode(Options.get("FixMode", None)),
+      FixFunction(Options.get("FixFunction", "gsl::at")),
+      FixFunctionEmptyArgs(Options.get("FixFunctionEmptyArgs", FixFunction)) {}
+
+void ProBoundsAvoidUncheckedContainerAccess::storeOptions(
+    ClangTidyOptions::OptionMap &Opts) {
+  Options.store(Opts, "ExcludeClasses",
+                utils::options::serializeStringList(ExcludedClasses));
+  Options.store(Opts, "FixMode", FixMode);
+  Options.store(Opts, "FixFunction", FixFunction);
+  Options.store(Opts, "FixFunctionEmptyArgs", FixFunctionEmptyArgs);
+}
+
+// TODO: if at() is defined in another class in the class hierarchy of the class
+// that defines the operator[] we matched on, findAlternative() will not detect
+// it.
+static const CXXMethodDecl *
+findAlternativeAt(const CXXMethodDecl *MatchedOperator) {
+  const CXXRecordDecl *Parent = MatchedOperator->getParent();
+  const QualType SubscriptThisObjType =
+      MatchedOperator->getFunctionObjectParameterReferenceType();
+
+  for (const CXXMethodDecl *Method : Parent->methods()) {
+    // Require 'Method' to be as accessible as 'MatchedOperator' or more
+    if (MatchedOperator->getAccess() < Method->getAccess())
+      continue;
+
+    if (MatchedOperator->isConst() != Method->isConst())
+      continue;
+
+    const QualType AtThisObjType =
+        Method->getFunctionObjectParameterReferenceType();
+    if (SubscriptThisObjType != AtThisObjType)
+      continue;
+
+    if (!Method->getNameInfo().getName().isIdentifier() ||
+        Method->getName() != "at")
+      continue;
+
+    const bool SameReturnType =
+        Method->getReturnType() == MatchedOperator->getReturnType();
+    if (!SameReturnType)
+      continue;
+
+    const bool SameNumberOfArguments =
+        Method->getNumParams() == MatchedOperator->getNumParams();
+    if (!SameNumberOfArguments)
+      continue;
+
+    for (unsigned ArgInd = 0; ArgInd < Method->getNumParams(); ArgInd++) {
+      const bool SameArgType =
+          Method->parameters()[ArgInd]->getOriginalType() ==
+          MatchedOperator->parameters()[ArgInd]->getOriginalType();
+      if (!SameArgType)
+        continue;
+    }
+
+    return Method;
+  }
+  return nullptr;
+}
+
+void ProBoundsAvoidUncheckedContainerAccess::registerMatchers(
+    MatchFinder *Finder) {
+  Finder->addMatcher(
+      mapAnyOf(cxxOperatorCallExpr, cxxMemberCallExpr)
+          .with(callee(
+              cxxMethodDecl(
+                  hasOverloadedOperatorName("[]"),
+                  anyOf(parameterCountIs(0), parameterCountIs(1)),
+                  unless(matchers::matchesAnyListedName(ExcludedClasses)))
+                  .bind("operator")))
+          .bind("caller"),
+      this);
+}
+
+void ProBoundsAvoidUncheckedContainerAccess::check(
+    const MatchFinder::MatchResult &Result) {
+
+  const auto *MatchedExpr = Result.Nodes.getNodeAs<CallExpr>("caller");
+
+  if (FixMode == None) {
+    diag(MatchedExpr->getCallee()->getBeginLoc(),
+         "possibly unsafe 'operator[]', consider bounds-safe alternatives")
+        << MatchedExpr->getCallee()->getSourceRange();
+    return;
+  }
+
+  if (const auto *OCE = dyn_cast<CXXOperatorCallExpr>(MatchedExpr)) {
+    // Case: a[i]
+    const auto LeftBracket = SourceRange(OCE->getCallee()->getBeginLoc(),
+                                         OCE->getCallee()->getBeginLoc());
+    const auto RightBracket =
+        SourceRange(OCE->getOperatorLoc(), OCE->getOperatorLoc());
+
+    if (FixMode == At) {
+      // Case: a[i] => a.at(i)
+      const auto *MatchedOperator =
+          Result.Nodes.getNodeAs<CXXMethodDecl>("operator");
+      const CXXMethodDecl *Alternative = findAlternativeAt(MatchedOperator);
+
+      if (!Alternative) {
+        diag(MatchedExpr->getCallee()->getBeginLoc(),
+             "possibly unsafe 'operator[]', consider "
+             "bounds-safe alternatives")
+            << MatchedExpr->getCallee()->getSourceRange();
+        return;
+      }
+
+      diag(MatchedExpr->getCallee()->getBeginLoc(),
+           "possibly unsafe 'operator[]', consider "
+           "bounds-safe alternative 'at()'")
+          << MatchedExpr->getCallee()->getSourceRange()
+          << FixItHint::CreateReplacement(LeftBracket, ".at(")
+          << FixItHint::CreateReplacement(RightBracket, ")");
+
+      diag(Alternative->getBeginLoc(), "viable 'at()' is defined here",
+           DiagnosticIDs::Note)
+          << Alternative->getNameInfo().getSourceRange();
+
+    } else if (FixMode == Function) {
+      // Case: a[i] => f(a, i)
+      //
+      // Since C++23, the subscript operator may also be called without an
+      // argument, which makes the following distinction necessary
+      const bool EmptySubscript =
+          MatchedExpr->getDirectCallee()->getNumParams() == 0;
+
+      if (EmptySubscript) {
+        auto D = diag(MatchedExpr->getCallee()->getBeginLoc(),
+                      "possibly unsafe 'operator[]'%select{, use safe "
+                      "function '%1() instead|}0")
+                 << FixFunctionEmptyArgs.empty() << FixFunctionEmptyArgs.str()
+                 << MatchedExpr->getCallee()->getSourceRange();
+        if (!FixFunctionEmptyArgs.empty()) {
+          D << FixItHint::CreateInsertion(OCE->getArg(0)->getBeginLoc(),
+                                          FixFunctionEmptyArgs.str() + "(")
+            << FixItHint::CreateRemoval(LeftBracket)
+            << FixItHint::CreateReplacement(RightBracket, ")");
+        }
+      } else {
+        diag(MatchedExpr->getCallee()->getBeginLoc(),
+             "possibly unsafe 'operator[]', use safe function '%0()' instead")
+            << FixFunction.str() << MatchedExpr->getCallee()->getSourceRange()
+            << FixItHint::CreateInsertion(OCE->getArg(0)->getBeginLoc(),
+                                          FixFunction.str() + "(")
+            << FixItHint::CreateReplacement(LeftBracket, ", ")
+            << FixItHint::CreateReplacement(RightBracket, ")");
+      }
+    }
+  } else if (const auto *MCE = dyn_cast<CXXMemberCallExpr>(MatchedExpr)) {
+    // Case: a.operator[](i) or a->operator[](i)
+    const auto *Callee = dyn_cast<MemberExpr>(MCE->getCallee());
+
+    if (FixMode == At) {
+      // Cases: a.operator[](i) => a.at(i) and a->operator[](i) => a->at(i)
+
+      const auto *MatchedOperator =
+          Result.Nodes.getNodeAs<CXXMethodDecl>("operator");
+
+      const CXXMethodDecl *Alternative = findAlternativeAt(MatchedOperator);
+      if (!Alternative) {
+        diag(Callee->getBeginLoc(), "possibly unsafe 'operator[]', consider "
+                                    "bounds-safe alternative 'at()'")
+            << Callee->getSourceRange();
+        return;
+      }
+      diag(MatchedExpr->getCallee()->getBeginLoc(),
+           "possibly unsafe 'operator[]', consider "
+           "bounds-safe alternative 'at()'")
+          << FixItHint::CreateReplacement(
+                 SourceRange(Callee->getMemberLoc(), Callee->getEndLoc()),
+                 "at");
+
+      diag(Alternative->getBeginLoc(), "viable 'at()' defined here",
+           DiagnosticIDs::Note)
+          << Alternative->getNameInfo().getSourceRange();
+
+    } else if (FixMode == Function) {
+      // Cases: a.operator[](i) => f(a, i) and a->operator[](i) => f(*a, i)
+      const auto *Callee = dyn_cast<MemberExpr>(MCE->getCallee());
+
+      const bool EmptySubscript =
+          MCE->getMethodDecl()->getNumNonObjectParams() == 0;
+
+      std::string BeginInsertion =
+          (EmptySubscript ? FixFunctionEmptyArgs.str() : FixFunction.str()) +
+          "(";
+
+      if (Callee->isArrow())
+        BeginInsertion += "*";
+
+      // Since C++23, the subscript operator may also be called without an
+      // argument, which makes the following distinction necessary
+      if (EmptySubscript) {
+        auto D = diag(MatchedExpr->getCallee()->getBeginLoc(),
+                      "possibly unsafe 'operator[]'%select{, use safe "
+                      "function '%1()' instead|}0")
+                 << FixFunctionEmptyArgs.empty() << FixFunctionEmptyArgs.str()
+                 << Callee->getSourceRange();
+
+        if (!FixFunctionEmptyArgs.empty()) {
+          D << FixItHint::CreateInsertion(MatchedExpr->getBeginLoc(),
+                                          BeginInsertion)
+            << FixItHint::CreateRemoval(
+                   SourceRange(Callee->getOperatorLoc(),
+                               MCE->getRParenLoc().getLocWithOffset(-1)));
+        }
+      } else {
+        diag(Callee->getBeginLoc(),
+             "possibly unsafe 'operator[]', use safe function '%0()' instead")
+            << FixFunction.str() << Callee->getSourceRange()
+            << FixItHint::CreateInsertion(MatchedExpr->getBeginLoc(),
+                                          BeginInsertion)
+            << FixItHint::CreateReplacement(
+                   SourceRange(
+                       Callee->getOperatorLoc(),
+                       MCE->getArg(0)->getBeginLoc().getLocWithOffset(-1)),
+                   ", ");
+      }
+    }
+  }
+}
+
+} // namespace clang::tidy::cppcoreguidelines
+
+namespace clang::tidy {
+using P = cppcoreguidelines::ProBoundsAvoidUncheckedContainerAccess;
+
+llvm::ArrayRef<std::pair<P::FixModes, StringRef>>
+OptionEnumMapping<P::FixModes>::getEnumMapping() {
+  static constexpr std::pair<P::FixModes, StringRef> Mapping[] = {
+      {P::None, "none"}, {P::At, "at"}, {P::Function, "function"}};
+  return {Mapping};
+}
+} // namespace clang::tidy
diff --git a/clang-tidy/cppcoreguidelines/ProBoundsAvoidUncheckedContainerAccess.h b/clang-tidy/cppcoreguidelines/ProBoundsAvoidUncheckedContainerAccess.h
new file mode 100644
index 0000000..cfd52d6
--- /dev/null
+++ b/clang-tidy/cppcoreguidelines/ProBoundsAvoidUncheckedContainerAccess.h
@@ -0,0 +1,56 @@
+//===--- ProBoundsAvoidUncheckedContainerAccess.h - clang-tidy --*- C++ -*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_CPPCOREGUIDELINES_PRO_BOUNDS_AVOID_UNCHECKED_CONTAINER_ACCESS_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_CPPCOREGUIDELINES_PRO_BOUNDS_AVOID_UNCHECKED_CONTAINER_ACCESS_H
+
+#include "../ClangTidyCheck.h"
+
+namespace clang::tidy::cppcoreguidelines {
+
+/// Flags calls to operator[] in STL containers and suggests replacing it with
+/// safe alternatives.
+///
+/// See
+/// https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#slcon3-avoid-bounds-errors
+/// For the user-facing documentation see:
+/// http://clang.llvm.org/extra/clang-tidy/checks/cppcoreguidelines/pro-bounds-avoid-unchecked-container-access.html
+class ProBoundsAvoidUncheckedContainerAccess : public ClangTidyCheck {
+public:
+  ProBoundsAvoidUncheckedContainerAccess(StringRef Name,
+                                         ClangTidyContext *Context);
+  bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
+    return LangOpts.CPlusPlus;
+  }
+  void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+  void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+  void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
+
+  enum FixModes { None, At, Function };
+
+private:
+  // A list of class names that are excluded from the warning
+  std::vector<llvm::StringRef> ExcludedClasses;
+  // Setting which fix to suggest
+  FixModes FixMode;
+  llvm::StringRef FixFunction;
+  llvm::StringRef FixFunctionEmptyArgs;
+};
+} // namespace clang::tidy::cppcoreguidelines
+
+namespace clang::tidy {
+template <>
+struct OptionEnumMapping<
+    cppcoreguidelines::ProBoundsAvoidUncheckedContainerAccess::FixModes> {
+  static ArrayRef<std::pair<
+      cppcoreguidelines::ProBoundsAvoidUncheckedContainerAccess::FixModes,
+      StringRef>>
+  getEnumMapping();
+};
+} // namespace clang::tidy
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_CPPCOREGUIDELINES_PRO_BOUNDS_AVOID_UNCHECKED_CONTAINER_ACCESS_H
diff --git a/docs/ReleaseNotes.rst b/docs/ReleaseNotes.rst
index a67b151..4fee4f9 100644
--- a/docs/ReleaseNotes.rst
+++ b/docs/ReleaseNotes.rst
@@ -135,6 +135,13 @@
   Detects default initialization (to 0) of variables with ``enum`` type where
   the enum has no enumerator with value of 0.
 
+- New :doc:`cppcoreguidelines-pro-bounds-avoid-unchecked-container-access
+  <clang-tidy/checks/cppcoreguidelines/pro-bounds-avoid-unchecked-container-access>`
+  check.
+
+  Finds calls to ``operator[]`` in STL containers and suggests replacing them
+  with safe alternatives.
+
 - New :doc:`llvm-mlir-op-builder
   <clang-tidy/checks/llvm/use-new-mlir-op-builder>` check.
 
diff --git a/docs/clang-tidy/checks/cppcoreguidelines/pro-bounds-avoid-unchecked-container-access.rst b/docs/clang-tidy/checks/cppcoreguidelines/pro-bounds-avoid-unchecked-container-access.rst
new file mode 100644
index 0000000..556d902
--- /dev/null
+++ b/docs/clang-tidy/checks/cppcoreguidelines/pro-bounds-avoid-unchecked-container-access.rst
@@ -0,0 +1,64 @@
+.. title:: clang-tidy - cppcoreguidelines-pro-bounds-avoid-unchecked-container-access
+
+cppcoreguidelines-pro-bounds-avoid-unchecked-container-access
+=============================================================
+
+Finds calls to ``operator[]`` in STL containers and suggests replacing them
+with safe alternatives.
+Safe alternatives include STL ``at`` or GSL ``at`` functions, ``begin()`` or
+``end()`` functions, ``range-for`` loops, ``std::span``, or an appropriate
+function from ``<algorithms>``.
+
+For example, both
+
+.. code-block:: c++
+
+  std::vector<int> a;
+  int b = a[4];
+
+and
+
+.. code-block:: c++
+
+  std::unique_ptr<vector> a;
+  int b = a[0];
+
+will generate a warning.
+
+STL containers for which ``operator[]`` is well-defined for all inputs are excluded
+from this check (e.g.: ``std::map::operator[]``).
+
+This check enforces part of the `SL.con.3
+<https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#slcon3-avoid-bounds-errors>`
+guideline and is part of the `Bounds Safety (Bounds 4)
+<https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Pro-bounds-arrayindex>`
+profile from the C++ Core Guidelines.
+
+Options
+-------
+
+.. option:: ExcludeClasses
+
+    Semicolon-delimited list of class names for overwriting the default
+    exclusion list. The default is:
+    `::std::map;::std::unordered_map;::std::flat_map`.
+    
+.. option:: FixMode
+
+    Determines what fixes are suggested. Either `none`, `at` (use 
+    ``a.at(index)`` if a fitting function exists) or `function` (use a 
+    function ``f(a, index)``). The default is `none`.
+
+.. option:: FixFunction
+
+    The function to use in the `function` mode. For C++23 and beyond, the
+    passed function must support the empty subscript operator, i.e., the case
+    where ``a[]`` becomes ``f(a)``. :option:`FixFunctionEmptyArgs` can be
+    used to override the suggested function in that case. The default is `gsl::at`. 
+
+.. option:: FixFunctionEmptyArgs
+
+    The function to use in the `function` mode for the empty subscript operator
+    case in C++23 and beyond only. If no fixes should be made for empty
+    subscript operators, pass an empty string. In that case, only the warnings
+    will be printed. The default is the value of :option:`FixFunction`.
diff --git a/docs/clang-tidy/checks/list.rst b/docs/clang-tidy/checks/list.rst
index b096126..5e3ffc4 100644
--- a/docs/clang-tidy/checks/list.rst
+++ b/docs/clang-tidy/checks/list.rst
@@ -201,6 +201,7 @@
    :doc:`cppcoreguidelines-owning-memory <cppcoreguidelines/owning-memory>`,
    :doc:`cppcoreguidelines-prefer-member-initializer <cppcoreguidelines/prefer-member-initializer>`, "Yes"
    :doc:`cppcoreguidelines-pro-bounds-array-to-pointer-decay <cppcoreguidelines/pro-bounds-array-to-pointer-decay>`,
+   :doc:`cppcoreguidelines-pro-bounds-avoid-unchecked-container-access <cppcoreguidelines/pro-bounds-avoid-unchecked-container-access>`, "Yes"
    :doc:`cppcoreguidelines-pro-bounds-constant-array-index <cppcoreguidelines/pro-bounds-constant-array-index>`, "Yes"
    :doc:`cppcoreguidelines-pro-bounds-pointer-arithmetic <cppcoreguidelines/pro-bounds-pointer-arithmetic>`,
    :doc:`cppcoreguidelines-pro-type-const-cast <cppcoreguidelines/pro-type-const-cast>`,
diff --git a/test/clang-tidy/checkers/cppcoreguidelines/pro-bounds-avoid-unchecked-container-access.cpp b/test/clang-tidy/checkers/cppcoreguidelines/pro-bounds-avoid-unchecked-container-access.cpp
new file mode 100644
index 0000000..30d03bd
--- /dev/null
+++ b/test/clang-tidy/checkers/cppcoreguidelines/pro-bounds-avoid-unchecked-container-access.cpp
@@ -0,0 +1,341 @@
+// RUN: rm -rf %t && mkdir %t
+// RUN: split-file %s %t
+
+
+// RUN: %check_clang_tidy -std=c++11,c++14,c++17,c++20 -check-suffix=DEFAULT %t/cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.cpp \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access %t -- -- -I%t
+
+// RUN: %check_clang_tidy -std=c++11,c++14,c++17,c++20 -check-suffix=AT %t/cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.cpp \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access %t -- \
+// RUN: -config='{CheckOptions: {cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixMode: at}}' -- -I%t
+
+// RUN: %check_clang_tidy -std=c++11,c++14,c++17,c++20 -check-suffix=FUNC %t/cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.cpp \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access %t -- \
+// RUN: -config='{CheckOptions: {cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixMode: function, \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixFunction: "f"}}' -- -I%t
+
+
+// RUN: %check_clang_tidy -std=c++11,c++14,c++17,c++20 -check-suffix=DEFAULT-NO-EXCL %t/cppcoreguidelines-pro-bounds-avoid-unchecked-container-access-no-excl.cpp \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access %t -- \
+// RUN: -config='{CheckOptions: {cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.ExcludeClasses: ""}}' -- -I%t
+
+// RUN: %check_clang_tidy -std=c++11,c++14,c++17,c++20 -check-suffix=AT-NO-EXCL %t/cppcoreguidelines-pro-bounds-avoid-unchecked-container-access-no-excl.cpp \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access %t -- \
+// RUN: -config='{CheckOptions: {cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.ExcludeClasses: "", \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixMode: at}}' -- -I%t
+
+// RUN: %check_clang_tidy -std=c++11,c++14,c++17,c++20 -check-suffix=FUNC-NO-EXCL %t/cppcoreguidelines-pro-bounds-avoid-unchecked-container-access-no-excl.cpp \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access %t -- \
+// RUN: -config='{CheckOptions: {cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.ExcludeClasses: "", \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixMode: function, \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixFunction: "f"}}' -- -I%t
+
+
+// RUN: %check_clang_tidy -std=c++11,c++14,c++17,c++20 -check-suffix=DEFAULT-EXCL %t/cppcoreguidelines-pro-bounds-avoid-unchecked-container-access-excl.cpp \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access %t -- \
+// RUN: -config='{CheckOptions: {cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.ExcludeClasses: "ExcludedClass1;ExcludedClass2"}}' -- -I%t
+
+// RUN: %check_clang_tidy -std=c++11,c++14,c++17,c++20 -check-suffix=AT-EXCL %t/cppcoreguidelines-pro-bounds-avoid-unchecked-container-access-excl.cpp \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access %t -- \
+// RUN: -config='{CheckOptions: {cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.ExcludeClasses: "ExcludedClass1;ExcludedClass2", \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixMode: at}}' -- -I%t
+
+// RUN: %check_clang_tidy -std=c++11,c++14,c++17,c++20 -check-suffix=FUNC-EXCL %t/cppcoreguidelines-pro-bounds-avoid-unchecked-container-access-excl.cpp \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access %t -- \
+// RUN: -config='{CheckOptions: {cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.ExcludeClasses: "ExcludedClass1;ExcludedClass2", \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixMode: function, \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixFunction: "f"}}' -- -I%t
+
+
+// RUN: %check_clang_tidy -std=c++23 -check-suffixes=DEFAULT-CXX-23 %t/cppcoreguidelines-pro-bounds-avoid-unchecked-container-access-cxx-23.cpp \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access %t -- -- -I%t -DCXX_23=1
+
+// RUN: %check_clang_tidy -std=c++23 -check-suffixes=AT-CXX-23 %t/cppcoreguidelines-pro-bounds-avoid-unchecked-container-access-cxx-23.cpp \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access %t -- \
+// RUN: -config='{CheckOptions: {cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixMode: at}}' -- -I%t -DCXX_23=1 
+
+// RUN: %check_clang_tidy -std=c++23 -check-suffixes=FUNC-CXX-23 %t/cppcoreguidelines-pro-bounds-avoid-unchecked-container-access-cxx-23.cpp \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access %t -- \
+// RUN: -config='{CheckOptions: {cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixMode: function, \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixFunction: "f"}}' -- -I%t -DCXX_23=1 
+
+// RUN: %check_clang_tidy -std=c++23 -check-suffixes=FUNC-EMPTY-ARGS-CXX-23 %t/cppcoreguidelines-pro-bounds-avoid-unchecked-container-access-cxx-23.cpp \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access %t -- \
+// RUN: -config='{CheckOptions: {cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixMode: function, \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixFunction: "f", cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixFunctionEmptyArgs: "g", }}' -- -I%t -DCXX_23=1
+
+// RUN: %check_clang_tidy -std=c++23 -check-suffix=FUNC-EMPTY-ARGS-EMPTY-CXX-23 %t/cppcoreguidelines-pro-bounds-avoid-unchecked-container-access-cxx-23.cpp \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access %t -- \
+// RUN: -config='{CheckOptions: {cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixMode: function, \
+// RUN: cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.FixFunctionEmptyArgs: "", }}' -- -I%t -DCXX_23=1
+
+//--- cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.h
+
+namespace std {
+  template<typename T, unsigned size>
+  struct array {
+    T operator[](unsigned i) {
+      return T{1};
+    }
+    T at(unsigned i) {
+      return T{1};
+    }
+    T at() {
+      return T{1};
+    }
+  };
+
+  template<typename T, typename V>
+  struct map {
+    T operator[](unsigned i) {
+      return T{1};
+    }
+    T at(unsigned i) {
+      return T{1};
+    }
+  };
+
+  template<typename T>
+  struct unique_ptr {
+    T operator[](unsigned i) {
+      return T{1};
+    }
+  };
+
+  template<typename T>
+  struct span {
+    T operator[](unsigned i) {
+      return T{1};
+    }
+  };
+} // namespace std
+
+namespace json {
+  template<typename T>
+  struct node{
+    T operator[](unsigned i) {
+      return T{1};
+    }
+  };
+} // namespace json
+
+//--- cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.cpp
+
+#include "cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.h"
+
+struct SubClass : std::array<int, 3> {};
+
+template<class T> int f(T, unsigned){ return 0;}
+template<class T> int f(T){ return 0;}
+
+std::array<int, 3> a;
+
+auto b = a[0];
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:11: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT: auto b = a.at(0);
+// CHECK-FIXES-FUNC: auto b = f(a, 0);
+
+auto c = a[1+1];
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:11: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT: auto c = a.at(1+1);
+// CHECK-FIXES-FUNC: auto c = f(a, 1+1);
+
+constexpr int Index = 1;
+
+auto d = a[Index];
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:11: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT: auto d = a.at(Index);
+// CHECK-FIXES-FUNC: auto d = f(a, Index);
+
+int e(int Ind) {
+  return a[Ind];
+  // CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:11: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+  // CHECK-FIXES-AT: return a.at(Ind);
+  // CHECK-FIXES-FUNC: return f(a, Ind);
+}
+
+auto fa = (&a)->operator[](1);
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:11: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT: auto fa = (&a)->at(1);
+// CHECK-FIXES-FUNC: auto fa = f(*(&a), 1);
+
+auto fd = a.operator[](1);
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:11: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT: auto fd = a.at(1);
+// CHECK-FIXES-FUNC: auto fd = f(a, 1);
+
+
+
+auto g = a.at(0);
+
+std::unique_ptr<int> p;
+auto q = p[0];
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:11: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT: auto q = p[0];
+// CHECK-FIXES-FUNC: auto q = f(p, 0);
+
+std::span<int> s;
+auto t = s[0];
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:11: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT: auto t = s[0];
+// CHECK-FIXES-FUNC: auto t = f(s, 0);
+
+json::node<int> n;
+auto m = n[0];
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:11: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT: auto m = n[0];
+// CHECK-FIXES-FUNC: auto m = f(n, 0);
+
+SubClass Sub;
+auto r = Sub[0];
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:13: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT: auto r = Sub.at(0);
+// CHECK-FIXES-FUNC: auto r = f(Sub, 0);
+
+typedef std::array<int, 3> ar;
+ar BehindDef;
+auto u = BehindDef[0];
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:19: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT: auto u = BehindDef.at(0);
+// CHECK-FIXES-FUNC: auto u = f(BehindDef, 0);
+
+template<typename T> int TestTemplate(T t){
+  return t[0];
+  // CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:10: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+
+}
+
+
+auto v = TestTemplate<>(a);
+auto w = TestTemplate<>(p);
+
+#define SUBSCRIPT_BEHIND_MACRO(x) a[x]
+#define ARG_BEHIND_MACRO 0
+#define OBJECT_BEHIND_MACRO a
+
+auto m1 = SUBSCRIPT_BEHIND_MACRO(0);
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:11: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+
+auto m2 = a[ARG_BEHIND_MACRO];
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:12: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT: auto m2 = a.at(ARG_BEHIND_MACRO);
+// CHECK-FIXES-FUNC: auto m2 = f(a, ARG_BEHIND_MACRO);
+
+auto m3 = OBJECT_BEHIND_MACRO[0];
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:30: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT: auto m3 = OBJECT_BEHIND_MACRO.at(0);
+// CHECK-FIXES-FUNC: auto m3 = f(OBJECT_BEHIND_MACRO, 0);
+
+// Check that spacing does not invalidate the fixes 
+std::array<int , 3> longname;
+
+auto z1 = longname   [    0    ]  ;
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:22: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT: auto z1 = longname   .at(    0    )  ;
+// CHECK-FIXES-FUNC: auto z1 = f(longname   ,     0    )  ;
+auto z2 = longname   . operator[]   ( 0 );
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:11: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT: auto z2 = longname   . at   ( 0 );
+// CHECK-FIXES-FUNC: auto z2 = f(longname   ,  0 );
+auto z3 = (&longname)   -> operator[]   ( 0 );
+// CHECK-MESSAGES-DEFAULT: :[[@LINE-1]]:11: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT: auto z3 = (&longname)   -> at   ( 0 );
+// CHECK-FIXES-FUNC: auto z3 = f(*(&longname)   ,  0 );
+
+
+//--- cppcoreguidelines-pro-bounds-avoid-unchecked-container-access-no-excl.cpp
+
+#include "cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.h"
+
+class ExcludedClass1 {
+  public:
+    int operator[](unsigned i) {
+      return 1;
+    }
+    int at(unsigned i) {
+      return 1;
+    }
+};
+
+class ExcludedClass2 {
+  public:
+    int operator[](unsigned i) {
+      return 1;
+    }
+    int at(unsigned i) {
+      return 1;
+    }
+};
+
+ExcludedClass1 E1;
+auto x1 = E1[0];
+// CHECK-MESSAGES-DEFAULT-NO-EXCL: :[[@LINE-1]]:13: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT-NO-EXCL: auto x1 = E1.at(0);
+// CHECK-FIXES-FUNC-NO-EXCL: auto x1 = f(E1, 0);
+
+ExcludedClass2 E2;
+auto x2 = E2[0];
+// CHECK-MESSAGES-DEFAULT-NO-EXCL: :[[@LINE-1]]:13: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT-NO-EXCL: auto x2 = E2.at(0);
+// CHECK-FIXES-FUNC-NO-EXCL: auto x2 = f(E2, 0);
+
+std::map<int,int> TestMapNoExcl;
+auto y = TestMapNoExcl[0];
+// CHECK-MESSAGES-DEFAULT-NO-EXCL: :[[@LINE-1]]:23: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT-NO-EXCL: auto y = TestMapNoExcl.at(0);
+// CHECK-FIXES-FUNC-NO-EXCL: auto y = f(TestMapNoExcl, 0);
+
+
+//--- cppcoreguidelines-pro-bounds-avoid-unchecked-container-access-excl.cpp
+
+#include "cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.h"
+
+std::map<int,int> TestMapExcl;
+auto y = TestMapExcl[0];
+// CHECK-MESSAGES-DEFAULT-EXCL: :[[@LINE-1]]:21: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT-EXCL: auto y = TestMapExcl.at(0);   
+// CHECK-FIXES-FUNC-EXCL: auto y = f(TestMapExcl, 0); 
+
+
+//--- cppcoreguidelines-pro-bounds-avoid-unchecked-container-access-cxx-23.cpp
+#ifdef CXX_23
+#include "cppcoreguidelines-pro-bounds-avoid-unchecked-container-access.h"
+
+namespace std {
+  template<typename T, unsigned size>
+  struct array_cxx_23 {
+    T operator[]() {
+      return T{1};
+    }
+    T at() {
+      return T{1};
+    }
+  };
+};
+
+std::array_cxx_23<int, 3> a;
+
+auto b23 = a[];
+// CHECK-MESSAGES-DEFAULT-CXX-23: :[[@LINE-1]]:13: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT-CXX-23: auto b23 = a.at();
+// CHECK-FIXES-FUNC-CXX-23: auto b23 = f(a);
+// CHECK-FIXES-FUNC-EMPTY-ARGS-CXX-23: auto b23 = g(a);
+// CHECK-MESSAGES-FUNC-EMPTY-ARGS-EMPTY-CXX-23: :[[@LINE-5]]:13: warning: possibly unsafe 'operator[]' [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access] 
+// CHECK-MESSAGES-FUNC-EMPTY-ARGS-EMPTY-CXX-23-NOT: :[[@LINE-6]]:{{.*}}: note: FIX-IT applied suggested code changes 
+
+auto fa23 = (&a)->operator[]();
+// CHECK-MESSAGES-DEFAULT-CXX-23: :[[@LINE-1]]:13: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT-CXX-23: auto fa23 = (&a)->at();
+// CHECK-FIXES-FUNC-CXX-23: auto fa23 = f(*(&a));
+// CHECK-FIXES-FUNC-EMPTY-ARGS-CXX-23: auto fa23 = g(*(&a));
+// CHECK-MESSAGES-FUNC-EMPTY-ARGS-EMPTY-CXX-23: :[[@LINE-5]]:13: warning: possibly unsafe 'operator[]' [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access] 
+// CHECK-MESSAGES-FUNC-EMPTY-ARGS-EMPTY-CXX-23-NOT: :[[@LINE-6]]:{{.*}}: note: FIX-IT applied suggested code changes 
+
+auto fd23 = a.operator[]();
+// CHECK-MESSAGES-DEFAULT-CXX-23: :[[@LINE-1]]:13: warning: possibly unsafe 'operator[]', consider bounds-safe alternatives [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access]
+// CHECK-FIXES-AT-CXX-23: auto fd23 = a.at();
+// CHECK-FIXES-FUNC-CXX-23: auto fd23 = f(a);
+// CHECK-FIXES-FUNC-EMPTY-ARGS-CXX-23: auto fd23 = g(a);
+// CHECK-MESSAGES-FUNC-EMPTY-ARGS-EMPTY-CXX-23: :[[@LINE-5]]:13: warning: possibly unsafe 'operator[]' [cppcoreguidelines-pro-bounds-avoid-unchecked-container-access] 
+// CHECK-MESSAGES-FUNC-EMPTY-ARGS-EMPTY-CXX-23-NOT: :[[@LINE-6]]:{{.*}}: note: FIX-IT applied suggested code changes 
+#endif