This is part of the codebase cleanup described in [#166753](https://github.com/llvm/llvm-project/issues/166753).
264 lines
10 KiB
C++
264 lines
10 KiB
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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "ProBoundsAvoidUncheckedContainerAccessCheck.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";
|
|
|
|
ProBoundsAvoidUncheckedContainerAccessCheck::
|
|
ProBoundsAvoidUncheckedContainerAccessCheck(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 ProBoundsAvoidUncheckedContainerAccessCheck::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 ProBoundsAvoidUncheckedContainerAccessCheck::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 ProBoundsAvoidUncheckedContainerAccessCheck::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::ProBoundsAvoidUncheckedContainerAccessCheck;
|
|
|
|
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
|