Skip to content

Commit bc278d0

Browse files
paulhdkEugeneZelenkovbvictor
authored
[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 <[email protected]> Co-authored-by: Sebastian Wolf <[email protected]> --------- Co-authored-by: EugeneZelenko <[email protected]> Co-authored-by: Baranov Victor <[email protected]>
1 parent 134cd79 commit bc278d0

File tree

8 files changed

+735
-0
lines changed

8 files changed

+735
-0
lines changed

clang-tools-extra/clang-tidy/cppcoreguidelines/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ add_clang_library(clangTidyCppCoreGuidelinesModule STATIC
2121
OwningMemoryCheck.cpp
2222
PreferMemberInitializerCheck.cpp
2323
ProBoundsArrayToPointerDecayCheck.cpp
24+
ProBoundsAvoidUncheckedContainerAccess.cpp
2425
ProBoundsConstantArrayIndexCheck.cpp
2526
ProBoundsPointerArithmeticCheck.cpp
2627
ProTypeConstCastCheck.cpp

clang-tools-extra/clang-tidy/cppcoreguidelines/CppCoreGuidelinesTidyModule.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include "OwningMemoryCheck.h"
3737
#include "PreferMemberInitializerCheck.h"
3838
#include "ProBoundsArrayToPointerDecayCheck.h"
39+
#include "ProBoundsAvoidUncheckedContainerAccess.h"
3940
#include "ProBoundsConstantArrayIndexCheck.h"
4041
#include "ProBoundsPointerArithmeticCheck.h"
4142
#include "ProTypeConstCastCheck.h"
@@ -107,6 +108,8 @@ class CppCoreGuidelinesModule : public ClangTidyModule {
107108
"cppcoreguidelines-prefer-member-initializer");
108109
CheckFactories.registerCheck<ProBoundsArrayToPointerDecayCheck>(
109110
"cppcoreguidelines-pro-bounds-array-to-pointer-decay");
111+
CheckFactories.registerCheck<ProBoundsAvoidUncheckedContainerAccess>(
112+
"cppcoreguidelines-pro-bounds-avoid-unchecked-container-access");
110113
CheckFactories.registerCheck<ProBoundsConstantArrayIndexCheck>(
111114
"cppcoreguidelines-pro-bounds-constant-array-index");
112115
CheckFactories.registerCheck<ProBoundsPointerArithmeticCheck>(
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
//===--- ProBoundsAvoidUncheckedContainerAccess.cpp - clang-tidy ----------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "ProBoundsAvoidUncheckedContainerAccess.h"
10+
#include "../utils/Matchers.h"
11+
#include "../utils/OptionsUtils.h"
12+
#include "clang/ASTMatchers/ASTMatchFinder.h"
13+
#include "llvm/ADT/StringRef.h"
14+
15+
using namespace clang::ast_matchers;
16+
17+
namespace clang::tidy::cppcoreguidelines {
18+
19+
static constexpr llvm::StringRef DefaultExclusionStr =
20+
"::std::map;::std::unordered_map;::std::flat_map";
21+
22+
ProBoundsAvoidUncheckedContainerAccess::ProBoundsAvoidUncheckedContainerAccess(
23+
StringRef Name, ClangTidyContext *Context)
24+
: ClangTidyCheck(Name, Context),
25+
ExcludedClasses(utils::options::parseStringList(
26+
Options.get("ExcludeClasses", DefaultExclusionStr))),
27+
FixMode(Options.get("FixMode", None)),
28+
FixFunction(Options.get("FixFunction", "gsl::at")),
29+
FixFunctionEmptyArgs(Options.get("FixFunctionEmptyArgs", FixFunction)) {}
30+
31+
void ProBoundsAvoidUncheckedContainerAccess::storeOptions(
32+
ClangTidyOptions::OptionMap &Opts) {
33+
Options.store(Opts, "ExcludeClasses",
34+
utils::options::serializeStringList(ExcludedClasses));
35+
Options.store(Opts, "FixMode", FixMode);
36+
Options.store(Opts, "FixFunction", FixFunction);
37+
Options.store(Opts, "FixFunctionEmptyArgs", FixFunctionEmptyArgs);
38+
}
39+
40+
// TODO: if at() is defined in another class in the class hierarchy of the class
41+
// that defines the operator[] we matched on, findAlternative() will not detect
42+
// it.
43+
static const CXXMethodDecl *
44+
findAlternativeAt(const CXXMethodDecl *MatchedOperator) {
45+
const CXXRecordDecl *Parent = MatchedOperator->getParent();
46+
const QualType SubscriptThisObjType =
47+
MatchedOperator->getFunctionObjectParameterReferenceType();
48+
49+
for (const CXXMethodDecl *Method : Parent->methods()) {
50+
// Require 'Method' to be as accessible as 'MatchedOperator' or more
51+
if (MatchedOperator->getAccess() < Method->getAccess())
52+
continue;
53+
54+
if (MatchedOperator->isConst() != Method->isConst())
55+
continue;
56+
57+
const QualType AtThisObjType =
58+
Method->getFunctionObjectParameterReferenceType();
59+
if (SubscriptThisObjType != AtThisObjType)
60+
continue;
61+
62+
if (!Method->getNameInfo().getName().isIdentifier() ||
63+
Method->getName() != "at")
64+
continue;
65+
66+
const bool SameReturnType =
67+
Method->getReturnType() == MatchedOperator->getReturnType();
68+
if (!SameReturnType)
69+
continue;
70+
71+
const bool SameNumberOfArguments =
72+
Method->getNumParams() == MatchedOperator->getNumParams();
73+
if (!SameNumberOfArguments)
74+
continue;
75+
76+
for (unsigned ArgInd = 0; ArgInd < Method->getNumParams(); ArgInd++) {
77+
const bool SameArgType =
78+
Method->parameters()[ArgInd]->getOriginalType() ==
79+
MatchedOperator->parameters()[ArgInd]->getOriginalType();
80+
if (!SameArgType)
81+
continue;
82+
}
83+
84+
return Method;
85+
}
86+
return nullptr;
87+
}
88+
89+
void ProBoundsAvoidUncheckedContainerAccess::registerMatchers(
90+
MatchFinder *Finder) {
91+
Finder->addMatcher(
92+
mapAnyOf(cxxOperatorCallExpr, cxxMemberCallExpr)
93+
.with(callee(
94+
cxxMethodDecl(
95+
hasOverloadedOperatorName("[]"),
96+
anyOf(parameterCountIs(0), parameterCountIs(1)),
97+
unless(matchers::matchesAnyListedName(ExcludedClasses)))
98+
.bind("operator")))
99+
.bind("caller"),
100+
this);
101+
}
102+
103+
void ProBoundsAvoidUncheckedContainerAccess::check(
104+
const MatchFinder::MatchResult &Result) {
105+
106+
const auto *MatchedExpr = Result.Nodes.getNodeAs<CallExpr>("caller");
107+
108+
if (FixMode == None) {
109+
diag(MatchedExpr->getCallee()->getBeginLoc(),
110+
"possibly unsafe 'operator[]', consider bounds-safe alternatives")
111+
<< MatchedExpr->getCallee()->getSourceRange();
112+
return;
113+
}
114+
115+
if (const auto *OCE = dyn_cast<CXXOperatorCallExpr>(MatchedExpr)) {
116+
// Case: a[i]
117+
const auto LeftBracket = SourceRange(OCE->getCallee()->getBeginLoc(),
118+
OCE->getCallee()->getBeginLoc());
119+
const auto RightBracket =
120+
SourceRange(OCE->getOperatorLoc(), OCE->getOperatorLoc());
121+
122+
if (FixMode == At) {
123+
// Case: a[i] => a.at(i)
124+
const auto *MatchedOperator =
125+
Result.Nodes.getNodeAs<CXXMethodDecl>("operator");
126+
const CXXMethodDecl *Alternative = findAlternativeAt(MatchedOperator);
127+
128+
if (!Alternative) {
129+
diag(MatchedExpr->getCallee()->getBeginLoc(),
130+
"possibly unsafe 'operator[]', consider "
131+
"bounds-safe alternatives")
132+
<< MatchedExpr->getCallee()->getSourceRange();
133+
return;
134+
}
135+
136+
diag(MatchedExpr->getCallee()->getBeginLoc(),
137+
"possibly unsafe 'operator[]', consider "
138+
"bounds-safe alternative 'at()'")
139+
<< MatchedExpr->getCallee()->getSourceRange()
140+
<< FixItHint::CreateReplacement(LeftBracket, ".at(")
141+
<< FixItHint::CreateReplacement(RightBracket, ")");
142+
143+
diag(Alternative->getBeginLoc(), "viable 'at()' is defined here",
144+
DiagnosticIDs::Note)
145+
<< Alternative->getNameInfo().getSourceRange();
146+
147+
} else if (FixMode == Function) {
148+
// Case: a[i] => f(a, i)
149+
//
150+
// Since C++23, the subscript operator may also be called without an
151+
// argument, which makes the following distinction necessary
152+
const bool EmptySubscript =
153+
MatchedExpr->getDirectCallee()->getNumParams() == 0;
154+
155+
if (EmptySubscript) {
156+
auto D = diag(MatchedExpr->getCallee()->getBeginLoc(),
157+
"possibly unsafe 'operator[]'%select{, use safe "
158+
"function '%1() instead|}0")
159+
<< FixFunctionEmptyArgs.empty() << FixFunctionEmptyArgs.str()
160+
<< MatchedExpr->getCallee()->getSourceRange();
161+
if (!FixFunctionEmptyArgs.empty()) {
162+
D << FixItHint::CreateInsertion(OCE->getArg(0)->getBeginLoc(),
163+
FixFunctionEmptyArgs.str() + "(")
164+
<< FixItHint::CreateRemoval(LeftBracket)
165+
<< FixItHint::CreateReplacement(RightBracket, ")");
166+
}
167+
} else {
168+
diag(MatchedExpr->getCallee()->getBeginLoc(),
169+
"possibly unsafe 'operator[]', use safe function '%0()' instead")
170+
<< FixFunction.str() << MatchedExpr->getCallee()->getSourceRange()
171+
<< FixItHint::CreateInsertion(OCE->getArg(0)->getBeginLoc(),
172+
FixFunction.str() + "(")
173+
<< FixItHint::CreateReplacement(LeftBracket, ", ")
174+
<< FixItHint::CreateReplacement(RightBracket, ")");
175+
}
176+
}
177+
} else if (const auto *MCE = dyn_cast<CXXMemberCallExpr>(MatchedExpr)) {
178+
// Case: a.operator[](i) or a->operator[](i)
179+
const auto *Callee = dyn_cast<MemberExpr>(MCE->getCallee());
180+
181+
if (FixMode == At) {
182+
// Cases: a.operator[](i) => a.at(i) and a->operator[](i) => a->at(i)
183+
184+
const auto *MatchedOperator =
185+
Result.Nodes.getNodeAs<CXXMethodDecl>("operator");
186+
187+
const CXXMethodDecl *Alternative = findAlternativeAt(MatchedOperator);
188+
if (!Alternative) {
189+
diag(Callee->getBeginLoc(), "possibly unsafe 'operator[]', consider "
190+
"bounds-safe alternative 'at()'")
191+
<< Callee->getSourceRange();
192+
return;
193+
}
194+
diag(MatchedExpr->getCallee()->getBeginLoc(),
195+
"possibly unsafe 'operator[]', consider "
196+
"bounds-safe alternative 'at()'")
197+
<< FixItHint::CreateReplacement(
198+
SourceRange(Callee->getMemberLoc(), Callee->getEndLoc()),
199+
"at");
200+
201+
diag(Alternative->getBeginLoc(), "viable 'at()' defined here",
202+
DiagnosticIDs::Note)
203+
<< Alternative->getNameInfo().getSourceRange();
204+
205+
} else if (FixMode == Function) {
206+
// Cases: a.operator[](i) => f(a, i) and a->operator[](i) => f(*a, i)
207+
const auto *Callee = dyn_cast<MemberExpr>(MCE->getCallee());
208+
209+
const bool EmptySubscript =
210+
MCE->getMethodDecl()->getNumNonObjectParams() == 0;
211+
212+
std::string BeginInsertion =
213+
(EmptySubscript ? FixFunctionEmptyArgs.str() : FixFunction.str()) +
214+
"(";
215+
216+
if (Callee->isArrow())
217+
BeginInsertion += "*";
218+
219+
// Since C++23, the subscript operator may also be called without an
220+
// argument, which makes the following distinction necessary
221+
if (EmptySubscript) {
222+
auto D = diag(MatchedExpr->getCallee()->getBeginLoc(),
223+
"possibly unsafe 'operator[]'%select{, use safe "
224+
"function '%1()' instead|}0")
225+
<< FixFunctionEmptyArgs.empty() << FixFunctionEmptyArgs.str()
226+
<< Callee->getSourceRange();
227+
228+
if (!FixFunctionEmptyArgs.empty()) {
229+
D << FixItHint::CreateInsertion(MatchedExpr->getBeginLoc(),
230+
BeginInsertion)
231+
<< FixItHint::CreateRemoval(
232+
SourceRange(Callee->getOperatorLoc(),
233+
MCE->getRParenLoc().getLocWithOffset(-1)));
234+
}
235+
} else {
236+
diag(Callee->getBeginLoc(),
237+
"possibly unsafe 'operator[]', use safe function '%0()' instead")
238+
<< FixFunction.str() << Callee->getSourceRange()
239+
<< FixItHint::CreateInsertion(MatchedExpr->getBeginLoc(),
240+
BeginInsertion)
241+
<< FixItHint::CreateReplacement(
242+
SourceRange(
243+
Callee->getOperatorLoc(),
244+
MCE->getArg(0)->getBeginLoc().getLocWithOffset(-1)),
245+
", ");
246+
}
247+
}
248+
}
249+
}
250+
251+
} // namespace clang::tidy::cppcoreguidelines
252+
253+
namespace clang::tidy {
254+
using P = cppcoreguidelines::ProBoundsAvoidUncheckedContainerAccess;
255+
256+
llvm::ArrayRef<std::pair<P::FixModes, StringRef>>
257+
OptionEnumMapping<P::FixModes>::getEnumMapping() {
258+
static constexpr std::pair<P::FixModes, StringRef> Mapping[] = {
259+
{P::None, "none"}, {P::At, "at"}, {P::Function, "function"}};
260+
return {Mapping};
261+
}
262+
} // namespace clang::tidy
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//===--- ProBoundsAvoidUncheckedContainerAccess.h - clang-tidy --*- C++ -*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_CPPCOREGUIDELINES_PRO_BOUNDS_AVOID_UNCHECKED_CONTAINER_ACCESS_H
10+
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_CPPCOREGUIDELINES_PRO_BOUNDS_AVOID_UNCHECKED_CONTAINER_ACCESS_H
11+
12+
#include "../ClangTidyCheck.h"
13+
14+
namespace clang::tidy::cppcoreguidelines {
15+
16+
/// Flags calls to operator[] in STL containers and suggests replacing it with
17+
/// safe alternatives.
18+
///
19+
/// See
20+
/// https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#slcon3-avoid-bounds-errors
21+
/// For the user-facing documentation see:
22+
/// http://clang.llvm.org/extra/clang-tidy/checks/cppcoreguidelines/pro-bounds-avoid-unchecked-container-access.html
23+
class ProBoundsAvoidUncheckedContainerAccess : public ClangTidyCheck {
24+
public:
25+
ProBoundsAvoidUncheckedContainerAccess(StringRef Name,
26+
ClangTidyContext *Context);
27+
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
28+
return LangOpts.CPlusPlus;
29+
}
30+
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
31+
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
32+
void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
33+
34+
enum FixModes { None, At, Function };
35+
36+
private:
37+
// A list of class names that are excluded from the warning
38+
std::vector<llvm::StringRef> ExcludedClasses;
39+
// Setting which fix to suggest
40+
FixModes FixMode;
41+
llvm::StringRef FixFunction;
42+
llvm::StringRef FixFunctionEmptyArgs;
43+
};
44+
} // namespace clang::tidy::cppcoreguidelines
45+
46+
namespace clang::tidy {
47+
template <>
48+
struct OptionEnumMapping<
49+
cppcoreguidelines::ProBoundsAvoidUncheckedContainerAccess::FixModes> {
50+
static ArrayRef<std::pair<
51+
cppcoreguidelines::ProBoundsAvoidUncheckedContainerAccess::FixModes,
52+
StringRef>>
53+
getEnumMapping();
54+
};
55+
} // namespace clang::tidy
56+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_CPPCOREGUIDELINES_PRO_BOUNDS_AVOID_UNCHECKED_CONTAINER_ACCESS_H

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ New checks
135135
Detects default initialization (to 0) of variables with ``enum`` type where
136136
the enum has no enumerator with value of 0.
137137

138+
- New :doc:`cppcoreguidelines-pro-bounds-avoid-unchecked-container-access
139+
<clang-tidy/checks/cppcoreguidelines/pro-bounds-avoid-unchecked-container-access>`
140+
check.
141+
142+
Finds calls to ``operator[]`` in STL containers and suggests replacing them
143+
with safe alternatives.
144+
138145
- New :doc:`llvm-mlir-op-builder
139146
<clang-tidy/checks/llvm/use-new-mlir-op-builder>` check.
140147

0 commit comments

Comments
 (0)