| 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 | |