263 lines · cpp
1//===----------------------------------------------------------------------===//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-exception6//7//===----------------------------------------------------------------------===//8 9#include "ProBoundsAvoidUncheckedContainerAccessCheck.h"10#include "../utils/Matchers.h"11#include "../utils/OptionsUtils.h"12#include "clang/ASTMatchers/ASTMatchFinder.h"13#include "llvm/ADT/StringRef.h"14 15using namespace clang::ast_matchers;16 17namespace clang::tidy::cppcoreguidelines {18 19static constexpr llvm::StringRef DefaultExclusionStr =20 "::std::map;::std::unordered_map;::std::flat_map";21 22ProBoundsAvoidUncheckedContainerAccessCheck::23 ProBoundsAvoidUncheckedContainerAccessCheck(StringRef Name,24 ClangTidyContext *Context)25 : ClangTidyCheck(Name, Context),26 ExcludedClasses(utils::options::parseStringList(27 Options.get("ExcludeClasses", DefaultExclusionStr))),28 FixMode(Options.get("FixMode", None)),29 FixFunction(Options.get("FixFunction", "gsl::at")),30 FixFunctionEmptyArgs(Options.get("FixFunctionEmptyArgs", FixFunction)) {}31 32void ProBoundsAvoidUncheckedContainerAccessCheck::storeOptions(33 ClangTidyOptions::OptionMap &Opts) {34 Options.store(Opts, "ExcludeClasses",35 utils::options::serializeStringList(ExcludedClasses));36 Options.store(Opts, "FixMode", FixMode);37 Options.store(Opts, "FixFunction", FixFunction);38 Options.store(Opts, "FixFunctionEmptyArgs", FixFunctionEmptyArgs);39}40 41// TODO: if at() is defined in another class in the class hierarchy of the class42// that defines the operator[] we matched on, findAlternative() will not detect43// it.44static const CXXMethodDecl *45findAlternativeAt(const CXXMethodDecl *MatchedOperator) {46 const CXXRecordDecl *Parent = MatchedOperator->getParent();47 const QualType SubscriptThisObjType =48 MatchedOperator->getFunctionObjectParameterReferenceType();49 50 for (const CXXMethodDecl *Method : Parent->methods()) {51 // Require 'Method' to be as accessible as 'MatchedOperator' or more52 if (MatchedOperator->getAccess() < Method->getAccess())53 continue;54 55 if (MatchedOperator->isConst() != Method->isConst())56 continue;57 58 const QualType AtThisObjType =59 Method->getFunctionObjectParameterReferenceType();60 if (SubscriptThisObjType != AtThisObjType)61 continue;62 63 if (!Method->getNameInfo().getName().isIdentifier() ||64 Method->getName() != "at")65 continue;66 67 const bool SameReturnType =68 Method->getReturnType() == MatchedOperator->getReturnType();69 if (!SameReturnType)70 continue;71 72 const bool SameNumberOfArguments =73 Method->getNumParams() == MatchedOperator->getNumParams();74 if (!SameNumberOfArguments)75 continue;76 77 for (unsigned ArgInd = 0; ArgInd < Method->getNumParams(); ArgInd++) {78 const bool SameArgType =79 Method->parameters()[ArgInd]->getOriginalType() ==80 MatchedOperator->parameters()[ArgInd]->getOriginalType();81 if (!SameArgType)82 continue;83 }84 85 return Method;86 }87 return nullptr;88}89 90void ProBoundsAvoidUncheckedContainerAccessCheck::registerMatchers(91 MatchFinder *Finder) {92 Finder->addMatcher(93 mapAnyOf(cxxOperatorCallExpr, cxxMemberCallExpr)94 .with(callee(95 cxxMethodDecl(96 hasOverloadedOperatorName("[]"),97 anyOf(parameterCountIs(0), parameterCountIs(1)),98 unless(matchers::matchesAnyListedName(ExcludedClasses)))99 .bind("operator")))100 .bind("caller"),101 this);102}103 104void ProBoundsAvoidUncheckedContainerAccessCheck::check(105 const MatchFinder::MatchResult &Result) {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 an151 // argument, which makes the following distinction necessary152 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 an220 // argument, which makes the following distinction necessary221 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::cppcoreguidelines252 253namespace clang::tidy {254using P = cppcoreguidelines::ProBoundsAvoidUncheckedContainerAccessCheck;255 256llvm::ArrayRef<std::pair<P::FixModes, StringRef>>257OptionEnumMapping<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::tidy263