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 | |
39 | using namespace clang; |
40 | using namespace ento; |
41 | using namespace ast_matchers; |
42 | |
43 | namespace { |
44 | |
45 | // ID of a node at which the diagnostic would be emitted. |
46 | const char *WarnAtNode = "waitcall" ; |
47 | |
48 | class GCDAntipatternChecker : public Checker<check::ASTCodeBody> { |
49 | public: |
50 | void checkASTCodeBody(const Decl *D, |
51 | AnalysisManager &AM, |
52 | BugReporter &BR) const; |
53 | }; |
54 | |
55 | decltype(auto) callsName(const char *FunctionName) { |
56 | return callee(InnerMatcher: functionDecl(hasName(Name: FunctionName))); |
57 | } |
58 | |
59 | decltype(auto) equalsBoundArgDecl(int ArgIdx, const char *DeclName) { |
60 | return hasArgument(N: ArgIdx, InnerMatcher: ignoringParenCasts(InnerMatcher: declRefExpr( |
61 | to(InnerMatcher: varDecl(equalsBoundNode(ID: DeclName)))))); |
62 | } |
63 | |
64 | decltype(auto) bindAssignmentToDecl(const char *DeclName) { |
65 | return hasLHS(InnerMatcher: ignoringParenImpCasts( |
66 | InnerMatcher: declRefExpr(to(InnerMatcher: varDecl().bind(ID: 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). |
73 | static bool isTest(const Decl *D) { |
74 | if (const auto* ND = dyn_cast<NamedDecl>(Val: D)) { |
75 | std::string DeclName = ND->getNameAsString(); |
76 | if (StringRef(DeclName).starts_with(Prefix: "test" )) |
77 | return true; |
78 | } |
79 | if (const auto *OD = dyn_cast<ObjCMethodDecl>(Val: 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(Other: "test" ) || CN.contains_insensitive(Other: "mock" )) |
84 | return true; |
85 | } |
86 | } |
87 | return false; |
88 | } |
89 | |
90 | static auto findGCDAntiPatternWithSemaphore() -> decltype(compoundStmt()) { |
91 | |
92 | const char *SemaphoreBinding = "semaphore_name" ; |
93 | auto SemaphoreCreateM = callExpr(allOf( |
94 | callsName(FunctionName: "dispatch_semaphore_create" ), |
95 | hasArgument(N: 0, InnerMatcher: ignoringParenCasts(InnerMatcher: integerLiteral(equals(Value: 0)))))); |
96 | |
97 | auto SemaphoreBindingM = anyOf( |
98 | forEachDescendant( |
99 | varDecl(hasDescendant(SemaphoreCreateM)).bind(ID: SemaphoreBinding)), |
100 | forEachDescendant(binaryOperator(bindAssignmentToDecl(DeclName: SemaphoreBinding), |
101 | hasRHS(InnerMatcher: SemaphoreCreateM)))); |
102 | |
103 | auto HasBlockArgumentM = hasAnyArgument(InnerMatcher: hasType( |
104 | InnerMatcher: hasCanonicalType(InnerMatcher: blockPointerType()) |
105 | )); |
106 | |
107 | auto ArgCallsSignalM = hasAnyArgument(InnerMatcher: stmt(hasDescendant(callExpr( |
108 | allOf( |
109 | callsName(FunctionName: "dispatch_semaphore_signal" ), |
110 | equalsBoundArgDecl(ArgIdx: 0, DeclName: 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(FunctionName: "dispatch_semaphore_wait" ), |
126 | equalsBoundArgDecl(ArgIdx: 0, DeclName: SemaphoreBinding) |
127 | ) |
128 | ).bind(ID: WarnAtNode)); |
129 | |
130 | return compoundStmt( |
131 | SemaphoreBindingM, HasBlockCallingSignalM, SemaphoreWaitM); |
132 | } |
133 | |
134 | static auto findGCDAntiPatternWithGroup() -> decltype(compoundStmt()) { |
135 | |
136 | const char *GroupBinding = "group_name" ; |
137 | auto DispatchGroupCreateM = callExpr(callsName(FunctionName: "dispatch_group_create" )); |
138 | |
139 | auto GroupBindingM = anyOf( |
140 | forEachDescendant( |
141 | varDecl(hasDescendant(DispatchGroupCreateM)).bind(ID: GroupBinding)), |
142 | forEachDescendant(binaryOperator(bindAssignmentToDecl(DeclName: GroupBinding), |
143 | hasRHS(InnerMatcher: DispatchGroupCreateM)))); |
144 | |
145 | auto GroupEnterM = forEachDescendant( |
146 | stmt(callExpr(allOf(callsName(FunctionName: "dispatch_group_enter" ), |
147 | equalsBoundArgDecl(ArgIdx: 0, DeclName: GroupBinding))))); |
148 | |
149 | auto HasBlockArgumentM = hasAnyArgument(InnerMatcher: hasType( |
150 | InnerMatcher: hasCanonicalType(InnerMatcher: blockPointerType()) |
151 | )); |
152 | |
153 | auto ArgCallsSignalM = hasAnyArgument(InnerMatcher: stmt(hasDescendant(callExpr( |
154 | allOf( |
155 | callsName(FunctionName: "dispatch_group_leave" ), |
156 | equalsBoundArgDecl(ArgIdx: 0, DeclName: 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(FunctionName: "dispatch_group_wait" ), |
172 | equalsBoundArgDecl(ArgIdx: 0, DeclName: GroupBinding) |
173 | ) |
174 | ).bind(ID: WarnAtNode)); |
175 | |
176 | return compoundStmt(GroupBindingM, GroupEnterM, AcceptsBlockM, GroupWaitM); |
177 | } |
178 | |
179 | static 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>(ID: 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 | |
203 | void 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(Matcher: SemaphoreMatcherM, Node: *D->getBody(), Context&: AM.getASTContext()); |
213 | for (BoundNodes Match : Matches) |
214 | emitDiagnostics(Nodes: Match, Type: "semaphore" , BR, ADC, Checker: this); |
215 | |
216 | auto GroupMatcherM = findGCDAntiPatternWithGroup(); |
217 | Matches = match(Matcher: GroupMatcherM, Node: *D->getBody(), Context&: AM.getASTContext()); |
218 | for (BoundNodes Match : Matches) |
219 | emitDiagnostics(Nodes: Match, Type: "group" , BR, ADC, Checker: this); |
220 | } |
221 | |
222 | } // end of anonymous namespace |
223 | |
224 | void ento::registerGCDAntipattern(CheckerManager &Mgr) { |
225 | Mgr.registerChecker<GCDAntipatternChecker>(); |
226 | } |
227 | |
228 | bool ento::shouldRegisterGCDAntipattern(const CheckerManager &mgr) { |
229 | return true; |
230 | } |
231 | |