1 | //===- unittests/StaticAnalyzer/BlockEntranceCallbackTest.cpp -------------===// |
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 | #include "CheckerRegistration.h" |
10 | #include "clang/Analysis/AnalysisDeclContext.h" |
11 | #include "clang/Analysis/ProgramPoint.h" |
12 | #include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h" |
13 | #include "clang/StaticAnalyzer/Core/BugReporter/BugType.h" |
14 | #include "clang/StaticAnalyzer/Core/Checker.h" |
15 | #include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h" |
16 | #include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState_Fwd.h" |
17 | #include "clang/StaticAnalyzer/Frontend/AnalysisConsumer.h" |
18 | #include "clang/StaticAnalyzer/Frontend/CheckerRegistry.h" |
19 | #include "llvm/ADT/STLExtras.h" |
20 | #include "llvm/ADT/StringExtras.h" |
21 | #include "llvm/ADT/StringRef.h" |
22 | #include "llvm/ADT/Twine.h" |
23 | #include "llvm/Support/FormatVariadic.h" |
24 | #include "llvm/Support/raw_ostream.h" |
25 | #include "gtest/gtest.h" |
26 | |
27 | using namespace clang; |
28 | using namespace ento; |
29 | |
30 | namespace { |
31 | |
32 | class BlockEntranceCallbackTester final : public Checker<check::BlockEntrance> { |
33 | const BugType Bug{this, "BlockEntranceTester" }; |
34 | |
35 | public: |
36 | void checkBlockEntrance(const BlockEntrance &Entrance, |
37 | CheckerContext &C) const { |
38 | ExplodedNode *Node = C.generateNonFatalErrorNode(State: C.getState()); |
39 | if (!Node) |
40 | return; |
41 | |
42 | const auto *FD = |
43 | cast<FunctionDecl>(Val: C.getLocationContext()->getStackFrame()->getDecl()); |
44 | |
45 | std::string Description = llvm::formatv( |
46 | "Within '{0}' B{1} -> B{2}" , FD->getIdentifier()->getName(), |
47 | Entrance.getPreviousBlock()->getBlockID(), |
48 | Entrance.getBlock()->getBlockID()); |
49 | auto Report = |
50 | std::make_unique<PathSensitiveBugReport>(args: Bug, args&: Description, args&: Node); |
51 | C.emitReport(R: std::move(Report)); |
52 | } |
53 | }; |
54 | |
55 | class BranchConditionCallbackTester final |
56 | : public Checker<check::BranchCondition> { |
57 | const BugType Bug{this, "BranchConditionCallbackTester" }; |
58 | |
59 | public: |
60 | void checkBranchCondition(const Stmt *Condition, CheckerContext &C) const { |
61 | ExplodedNode *Node = C.generateNonFatalErrorNode(State: C.getState()); |
62 | if (!Node) |
63 | return; |
64 | const auto *FD = |
65 | cast<FunctionDecl>(Val: C.getLocationContext()->getStackFrame()->getDecl()); |
66 | |
67 | std::string Buffer = |
68 | (llvm::Twine("Within '" ) + FD->getIdentifier()->getName() + |
69 | "': branch condition '" ) |
70 | .str(); |
71 | llvm::raw_string_ostream OS(Buffer); |
72 | Condition->printPretty(OS, /*Helper=*/nullptr, |
73 | Policy: C.getASTContext().getPrintingPolicy()); |
74 | OS << "'" ; |
75 | auto Report = std::make_unique<PathSensitiveBugReport>(args: Bug, args&: Buffer, args&: Node); |
76 | C.emitReport(R: std::move(Report)); |
77 | |
78 | C.addTransition(); |
79 | } |
80 | }; |
81 | |
82 | template <typename Checker> void registerChecker(CheckerManager &Mgr) { |
83 | Mgr.registerChecker<Checker>(); |
84 | } |
85 | |
86 | bool shouldAlwaysRegister(const CheckerManager &) { return true; } |
87 | |
88 | void addBlockEntranceTester(AnalysisASTConsumer &AnalysisConsumer, |
89 | AnalyzerOptions &AnOpts) { |
90 | AnOpts.CheckersAndPackages.emplace_back(args: "test.BlockEntranceTester" , args: true); |
91 | AnalysisConsumer.AddCheckerRegistrationFn(Fn: [](CheckerRegistry &Registry) { |
92 | Registry.addChecker(Fn: ®isterChecker<BlockEntranceCallbackTester>, |
93 | sfn: &shouldAlwaysRegister, FullName: "test.BlockEntranceTester" , |
94 | Desc: "EmptyDescription" , DocsUri: "EmptyDocsUri" , |
95 | /*IsHidden=*/false); |
96 | }); |
97 | } |
98 | |
99 | void addBranchConditionTester(AnalysisASTConsumer &AnalysisConsumer, |
100 | AnalyzerOptions &AnOpts) { |
101 | AnOpts.CheckersAndPackages.emplace_back(args: "test.BranchConditionTester" , args: true); |
102 | AnalysisConsumer.AddCheckerRegistrationFn(Fn: [](CheckerRegistry &Registry) { |
103 | Registry.addChecker(Fn: ®isterChecker<BranchConditionCallbackTester>, |
104 | sfn: &shouldAlwaysRegister, FullName: "test.BranchConditionTester" , |
105 | Desc: "EmptyDescription" , DocsUri: "EmptyDocsUri" , |
106 | /*IsHidden=*/false); |
107 | }); |
108 | } |
109 | |
110 | llvm::SmallVector<StringRef> parseEachDiag(StringRef Diags) { |
111 | llvm::SmallVector<StringRef> Fragments; |
112 | llvm::SplitString(Source: Diags, OutFragments&: Fragments, Delimiters: "\n" ); |
113 | // Drop the prefix like "test.BlockEntranceTester: " from each fragment. |
114 | for (StringRef &Fragment : Fragments) { |
115 | Fragment = Fragment.drop_until(F: [](char Ch) { return Ch == ' '; }); |
116 | Fragment.consume_front(Prefix: " " ); |
117 | } |
118 | llvm::sort(C&: Fragments); |
119 | return Fragments; |
120 | } |
121 | |
122 | template <AddCheckerFn Fn = addBlockEntranceTester, AddCheckerFn... Fns> |
123 | bool runChecker(const std::string &Code, std::string &Diags) { |
124 | std::string RawDiags; |
125 | bool Res = runCheckerOnCode<Fn, Fns...>(Code, RawDiags, |
126 | /*OnlyEmitWarnings=*/true); |
127 | llvm::raw_string_ostream OS(Diags); |
128 | llvm::interleave(c: parseEachDiag(Diags: RawDiags), os&: OS, separator: "\n" ); |
129 | return Res; |
130 | } |
131 | |
132 | [[maybe_unused]] void dumpCFGAndEgraph(AnalysisASTConsumer &AnalysisConsumer, |
133 | AnalyzerOptions &AnOpts) { |
134 | AnOpts.CheckersAndPackages.emplace_back(args: "debug.DumpCFG" , args: true); |
135 | AnOpts.CheckersAndPackages.emplace_back(args: "debug.ViewExplodedGraph" , args: true); |
136 | } |
137 | |
138 | /// Use this instead of \c runChecker to enable the debugging a test case. |
139 | template <AddCheckerFn... Fns> |
140 | [[maybe_unused]] bool debugChecker(const std::string &Code, |
141 | std::string &Diags) { |
142 | return runChecker<dumpCFGAndEgraph, Fns...>(Code, Diags); |
143 | } |
144 | |
145 | std::string expected(SmallVector<StringRef> Diags) { |
146 | llvm::sort(C&: Diags); |
147 | std::string Result; |
148 | llvm::raw_string_ostream OS(Result); |
149 | llvm::interleave(c: Diags, os&: OS, separator: "\n" ); |
150 | return Result; |
151 | } |
152 | |
153 | TEST(BlockEntranceTester, FromEntryToExit) { |
154 | constexpr auto Code = R"cpp( |
155 | void top() { |
156 | // empty |
157 | })cpp" ; |
158 | |
159 | std::string Diags; |
160 | // Use "debugChecker" instead of "runChecker" for debugging. |
161 | EXPECT_TRUE(runChecker(Code, Diags)); |
162 | EXPECT_EQ(expected({"Within 'top' B1 -> B0" }), Diags); |
163 | } |
164 | |
165 | TEST(BlockEntranceTester, SingleOpaqueIfCondition) { |
166 | constexpr auto Code = R"cpp( |
167 | bool coin(); |
168 | int glob; |
169 | void top() { |
170 | if (coin()) { |
171 | glob = 1; |
172 | } else { |
173 | glob = 2; |
174 | } |
175 | glob = 3; |
176 | })cpp" ; |
177 | |
178 | std::string Diags; |
179 | // Use "debugChecker" instead of "runChecker" for debugging. |
180 | EXPECT_TRUE(runChecker(Code, Diags)); |
181 | EXPECT_EQ(expected({ |
182 | "Within 'top' B1 -> B0" , |
183 | "Within 'top' B2 -> B1" , |
184 | "Within 'top' B3 -> B1" , |
185 | "Within 'top' B4 -> B2" , |
186 | "Within 'top' B4 -> B3" , |
187 | "Within 'top' B5 -> B4" , |
188 | }), |
189 | Diags); |
190 | // entry true exit |
191 | // B5 -------> B4 --> B2 --> B1 --> B0 |
192 | // | ^ |
193 | // | false | |
194 | // v | |
195 | // B3 -----------------------+ |
196 | } |
197 | |
198 | TEST(BlockEntranceTester, TrivialIfCondition) { |
199 | constexpr auto Code = R"cpp( |
200 | bool coin(); |
201 | int glob; |
202 | void top() { |
203 | int cond = true; |
204 | if (cond) { |
205 | glob = 1; |
206 | } else { |
207 | glob = 2; |
208 | } |
209 | glob = 3; |
210 | })cpp" ; |
211 | |
212 | std::string Diags; |
213 | // Use "debugChecker" instead of "runChecker" for debugging. |
214 | EXPECT_TRUE(runChecker(Code, Diags)); |
215 | EXPECT_EQ(expected({ |
216 | "Within 'top' B1 -> B0" , |
217 | "Within 'top' B3 -> B1" , |
218 | "Within 'top' B4 -> B3" , |
219 | "Within 'top' B5 -> B4" , |
220 | }), |
221 | Diags); |
222 | // entry true exit |
223 | // B5 ----------> B4 --> B3 --> B1 --> B0 |
224 | } |
225 | |
226 | TEST(BlockEntranceTester, AcrossFunctions) { |
227 | constexpr auto Code = R"cpp( |
228 | bool coin(); |
229 | int glob; |
230 | void nested() { glob = 1; } |
231 | void top() { |
232 | glob = 0; |
233 | nested(); |
234 | glob = 2; |
235 | })cpp" ; |
236 | |
237 | std::string Diags; |
238 | // Use "debugChecker" instead of "runChecker" for debugging. |
239 | EXPECT_TRUE(runChecker(Code, Diags)); |
240 | EXPECT_EQ( |
241 | expected({ |
242 | // Going from the "top" entry artificial node to the "top" body. |
243 | // Ideally, we shouldn't observe this edge because it's artificial. |
244 | "Within 'top' B2 -> B1" , |
245 | |
246 | // We encounter the call to "nested()" in the "top" body, thus we have |
247 | // a "CallEnter" node, but importantly, we also elide the transition |
248 | // to the "entry" node of "nested()". |
249 | // We only see the edge from the "nested()" entry to the "nested()" |
250 | // body: |
251 | "Within 'nested' B2 -> B1" , |
252 | |
253 | // Once we return from "nested()", we transition to the "exit" node of |
254 | // "nested()": |
255 | "Within 'nested' B1 -> B0" , |
256 | |
257 | // We will eventually return to the "top" body, thus we transition to |
258 | // its "exit" node: |
259 | "Within 'top' B1 -> B0" , |
260 | }), |
261 | Diags); |
262 | } |
263 | |
264 | TEST(BlockEntranceTester, ShortCircuitingLogicalOperator) { |
265 | constexpr auto Code = R"cpp( |
266 | bool coin(); |
267 | void top(int x) { |
268 | int v = 0; |
269 | if (coin() && (v = x)) { |
270 | v = 2; |
271 | } |
272 | v = 3; |
273 | })cpp" ; |
274 | // coin(): false |
275 | // +--------------------------------+ |
276 | // entry | v exit |
277 | // +----+ +----+ +----+ +----+ +----+ +----+ |
278 | // | B5 | --> | B4 | --> | B3 | --> | B2 | --> | B1 | --> | B0 | |
279 | // +----+ +----+ +----+ +----+ +----+ +----+ |
280 | // | ^ |
281 | // +---------------------+ |
282 | // (v = x): false |
283 | |
284 | std::string Diags; |
285 | // Use "debugChecker" instead of "runChecker" for debugging. |
286 | EXPECT_TRUE(runChecker(Code, Diags)); |
287 | EXPECT_EQ(expected({ |
288 | "Within 'top' B1 -> B0" , |
289 | "Within 'top' B2 -> B1" , |
290 | "Within 'top' B3 -> B1" , |
291 | "Within 'top' B3 -> B2" , |
292 | "Within 'top' B4 -> B1" , |
293 | "Within 'top' B4 -> B3" , |
294 | "Within 'top' B5 -> B4" , |
295 | }), |
296 | Diags); |
297 | } |
298 | |
299 | TEST(BlockEntranceTester, Switch) { |
300 | constexpr auto Code = R"cpp( |
301 | bool coin(); |
302 | int top(int x) { |
303 | int v = 0; |
304 | switch (x) { |
305 | case 1: v = 10; break; |
306 | case 2: v = 20; break; |
307 | default: v = 30; break; |
308 | } |
309 | return v; |
310 | })cpp" ; |
311 | // +----+ |
312 | // | B5 | -------------------------+ |
313 | // +----+ | |
314 | // ^ [case 1] | |
315 | // entry | v exit |
316 | // +----+ +----+ [default] +----+ +----+ +----+ |
317 | // | B6 | --> | B2 | ----------> | B3 | --> | B1 | --> | B0 | |
318 | // +----+ +----+ +----+ +----+ +----+ |
319 | // | ^ |
320 | // v [case 2] | |
321 | // +----+ | |
322 | // | B4 | -------------------------+ |
323 | // +----+ |
324 | |
325 | std::string Diags; |
326 | // Use "debugChecker" instead of "runChecker" for debugging. |
327 | EXPECT_TRUE(runChecker(Code, Diags)); |
328 | EXPECT_EQ(expected({ |
329 | "Within 'top' B1 -> B0" , |
330 | "Within 'top' B2 -> B3" , |
331 | "Within 'top' B2 -> B4" , |
332 | "Within 'top' B2 -> B5" , |
333 | "Within 'top' B3 -> B1" , |
334 | "Within 'top' B4 -> B1" , |
335 | "Within 'top' B5 -> B1" , |
336 | "Within 'top' B6 -> B2" , |
337 | }), |
338 | Diags); |
339 | } |
340 | |
341 | TEST(BlockEntranceTester, BlockEntranceVSBranchCondition) { |
342 | constexpr auto Code = R"cpp( |
343 | bool coin(); |
344 | int top(int x) { |
345 | int v = 0; |
346 | switch (x) { |
347 | default: v = 30; break; |
348 | } |
349 | if (x == 6) { |
350 | v = 40; |
351 | } |
352 | return v; |
353 | })cpp" ; |
354 | std::string Diags; |
355 | // Use "debugChecker" instead of "runChecker" for debugging. |
356 | EXPECT_TRUE((runChecker<addBlockEntranceTester, addBranchConditionTester>( |
357 | Code, Diags))); |
358 | EXPECT_EQ(expected({ |
359 | "Within 'top' B1 -> B0" , |
360 | "Within 'top' B2 -> B1" , |
361 | "Within 'top' B3 -> B1" , |
362 | "Within 'top' B3 -> B2" , |
363 | "Within 'top' B4 -> B5" , |
364 | "Within 'top' B5 -> B3" , |
365 | "Within 'top' B6 -> B4" , |
366 | "Within 'top': branch condition 'x == 6'" , |
367 | }), |
368 | Diags); |
369 | } |
370 | |
371 | } // namespace |
372 | |