231 lines · cpp
1//===- GCDAntipatternChecker.cpp ---------------------------------*- 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-exception6//7//===----------------------------------------------------------------------===//8//9// This file defines GCDAntipatternChecker which checks against a common10// antipattern when synchronous API is emulated from asynchronous callbacks11// using a semaphore:12//13// dispatch_semaphore_t sema = dispatch_semaphore_create(0);14//15// AnyCFunctionCall(^{16// // code…17// dispatch_semaphore_signal(sema);18// })19// dispatch_semaphore_wait(sema, *)20//21// Such code is a common performance problem, due to inability of GCD to22// properly handle QoS when a combination of queues and semaphores is used.23// Good code would either use asynchronous API (when available), or perform24// the necessary action in asynchronous callback.25//26// Currently, the check is performed using a simple heuristical AST pattern27// matching.28//29//===----------------------------------------------------------------------===//30 31#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"32#include "clang/ASTMatchers/ASTMatchFinder.h"33#include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h"34#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"35#include "clang/StaticAnalyzer/Core/Checker.h"36#include "clang/StaticAnalyzer/Core/PathSensitive/AnalysisManager.h"37#include "llvm/Support/Debug.h"38 39using namespace clang;40using namespace ento;41using namespace ast_matchers;42 43namespace {44 45// ID of a node at which the diagnostic would be emitted.46const char *WarnAtNode = "waitcall";47 48class GCDAntipatternChecker : public Checker<check::ASTCodeBody> {49public:50 void checkASTCodeBody(const Decl *D,51 AnalysisManager &AM,52 BugReporter &BR) const;53};54 55decltype(auto) callsName(const char *FunctionName) {56 return callee(functionDecl(hasName(FunctionName)));57}58 59decltype(auto) equalsBoundArgDecl(int ArgIdx, const char *DeclName) {60 return hasArgument(ArgIdx, ignoringParenCasts(declRefExpr(61 to(varDecl(equalsBoundNode(DeclName))))));62}63 64decltype(auto) bindAssignmentToDecl(const char *DeclName) {65 return hasLHS(ignoringParenImpCasts(66 declRefExpr(to(varDecl().bind(DeclName)))));67}68 69/// The pattern is very common in tests, and it is OK to use it there.70/// We have to heuristics for detecting tests: method name starts with "test"71/// (used in XCTest), and a class name contains "mock" or "test" (used in72/// helpers which are not tests themselves, but used exclusively in tests).73static bool isTest(const Decl *D) {74 if (const auto* ND = dyn_cast<NamedDecl>(D)) {75 std::string DeclName = ND->getNameAsString();76 if (StringRef(DeclName).starts_with("test"))77 return true;78 }79 if (const auto *OD = dyn_cast<ObjCMethodDecl>(D)) {80 if (const auto *CD = dyn_cast<ObjCContainerDecl>(OD->getParent())) {81 std::string ContainerName = CD->getNameAsString();82 StringRef CN(ContainerName);83 if (CN.contains_insensitive("test") || CN.contains_insensitive("mock"))84 return true;85 }86 }87 return false;88}89 90static auto findGCDAntiPatternWithSemaphore() -> decltype(compoundStmt()) {91 92 const char *SemaphoreBinding = "semaphore_name";93 auto SemaphoreCreateM = callExpr(allOf(94 callsName("dispatch_semaphore_create"),95 hasArgument(0, ignoringParenCasts(integerLiteral(equals(0))))));96 97 auto SemaphoreBindingM = anyOf(98 forEachDescendant(99 varDecl(hasDescendant(SemaphoreCreateM)).bind(SemaphoreBinding)),100 forEachDescendant(binaryOperator(bindAssignmentToDecl(SemaphoreBinding),101 hasRHS(SemaphoreCreateM))));102 103 auto HasBlockArgumentM = hasAnyArgument(hasType(104 hasCanonicalType(blockPointerType())105 ));106 107 auto ArgCallsSignalM = hasAnyArgument(stmt(hasDescendant(callExpr(108 allOf(109 callsName("dispatch_semaphore_signal"),110 equalsBoundArgDecl(0, SemaphoreBinding)111 )))));112 113 auto HasBlockAndCallsSignalM = allOf(HasBlockArgumentM, ArgCallsSignalM);114 115 auto HasBlockCallingSignalM =116 forEachDescendant(117 stmt(anyOf(118 callExpr(HasBlockAndCallsSignalM),119 objcMessageExpr(HasBlockAndCallsSignalM)120 )));121 122 auto SemaphoreWaitM = forEachDescendant(123 callExpr(124 allOf(125 callsName("dispatch_semaphore_wait"),126 equalsBoundArgDecl(0, SemaphoreBinding)127 )128 ).bind(WarnAtNode));129 130 return compoundStmt(131 SemaphoreBindingM, HasBlockCallingSignalM, SemaphoreWaitM);132}133 134static auto findGCDAntiPatternWithGroup() -> decltype(compoundStmt()) {135 136 const char *GroupBinding = "group_name";137 auto DispatchGroupCreateM = callExpr(callsName("dispatch_group_create"));138 139 auto GroupBindingM = anyOf(140 forEachDescendant(141 varDecl(hasDescendant(DispatchGroupCreateM)).bind(GroupBinding)),142 forEachDescendant(binaryOperator(bindAssignmentToDecl(GroupBinding),143 hasRHS(DispatchGroupCreateM))));144 145 auto GroupEnterM = forEachDescendant(146 stmt(callExpr(allOf(callsName("dispatch_group_enter"),147 equalsBoundArgDecl(0, GroupBinding)))));148 149 auto HasBlockArgumentM = hasAnyArgument(hasType(150 hasCanonicalType(blockPointerType())151 ));152 153 auto ArgCallsSignalM = hasAnyArgument(stmt(hasDescendant(callExpr(154 allOf(155 callsName("dispatch_group_leave"),156 equalsBoundArgDecl(0, GroupBinding)157 )))));158 159 auto HasBlockAndCallsLeaveM = allOf(HasBlockArgumentM, ArgCallsSignalM);160 161 auto AcceptsBlockM =162 forEachDescendant(163 stmt(anyOf(164 callExpr(HasBlockAndCallsLeaveM),165 objcMessageExpr(HasBlockAndCallsLeaveM)166 )));167 168 auto GroupWaitM = forEachDescendant(169 callExpr(170 allOf(171 callsName("dispatch_group_wait"),172 equalsBoundArgDecl(0, GroupBinding)173 )174 ).bind(WarnAtNode));175 176 return compoundStmt(GroupBindingM, GroupEnterM, AcceptsBlockM, GroupWaitM);177}178 179static void emitDiagnostics(const BoundNodes &Nodes,180 const char* Type,181 BugReporter &BR,182 AnalysisDeclContext *ADC,183 const GCDAntipatternChecker *Checker) {184 const auto *SW = Nodes.getNodeAs<CallExpr>(WarnAtNode);185 assert(SW);186 187 std::string Diagnostics;188 llvm::raw_string_ostream OS(Diagnostics);189 OS << "Waiting on a callback using a " << Type << " creates useless threads "190 << "and is subject to priority inversion; consider "191 << "using a synchronous API or changing the caller to be asynchronous";192 193 BR.EmitBasicReport(194 ADC->getDecl(),195 Checker,196 /*Name=*/"GCD performance anti-pattern",197 /*BugCategory=*/"Performance",198 OS.str(),199 PathDiagnosticLocation::createBegin(SW, BR.getSourceManager(), ADC),200 SW->getSourceRange());201}202 203void GCDAntipatternChecker::checkASTCodeBody(const Decl *D,204 AnalysisManager &AM,205 BugReporter &BR) const {206 if (isTest(D))207 return;208 209 AnalysisDeclContext *ADC = AM.getAnalysisDeclContext(D);210 211 auto SemaphoreMatcherM = findGCDAntiPatternWithSemaphore();212 auto Matches = match(SemaphoreMatcherM, *D->getBody(), AM.getASTContext());213 for (BoundNodes Match : Matches)214 emitDiagnostics(Match, "semaphore", BR, ADC, this);215 216 auto GroupMatcherM = findGCDAntiPatternWithGroup();217 Matches = match(GroupMatcherM, *D->getBody(), AM.getASTContext());218 for (BoundNodes Match : Matches)219 emitDiagnostics(Match, "group", BR, ADC, this);220}221 222} // end of anonymous namespace223 224void ento::registerGCDAntipattern(CheckerManager &Mgr) {225 Mgr.registerChecker<GCDAntipatternChecker>();226}227 228bool ento::shouldRegisterGCDAntipattern(const CheckerManager &mgr) {229 return true;230}231