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-exception
6//
7//===----------------------------------------------------------------------===//
8//
9// This file defines GCDAntipatternChecker which checks against a common
10// antipattern when synchronous API is emulated from asynchronous callbacks
11// 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 to
22// properly handle QoS when a combination of queues and semaphores is used.
23// Good code would either use asynchronous API (when available), or perform
24// the necessary action in asynchronous callback.
25//
26// Currently, the check is performed using a simple heuristical AST pattern
27// 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 in
72/// 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 namespace
223
224void ento::registerGCDAntipattern(CheckerManager &Mgr) {
225  Mgr.registerChecker<GCDAntipatternChecker>();
226}
227
228bool ento::shouldRegisterGCDAntipattern(const CheckerManager &mgr) {
229  return true;
230}
231